# Local Business Leads - Emails, Phones, Websites (OSM) (`tiger19935/local-business-leads`) Actor

Find local businesses by category and city from OpenStreetMap: name, address, phone, website, social links and MX-verified emails read from their own sites. Pay only for businesses with contact data. Not a Google Maps scraper.

- **URL**: https://apify.com/tiger19935/local-business-leads.md
- **Developed by:** [Andrew Boiko](https://apify.com/tiger19935) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.20 / 1,000 business with contact data

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Local Business Leads - Emails, Phones, Websites (OpenStreetMap)

Build a list of local businesses for any category and place: **name, address, phone, website, social links and a public email address** that is read from the business's own website and checked for a working mail server (MX). The data comes from **OpenStreetMap**, the open map database, so there is no Google Maps scraping, no login and no API key.

> Not a Google Maps scraper. OpenStreetMap is community-maintained: coverage and contact details differ a lot between countries and cities. Read the "Limits" section before you buy a big list.

### Who is it for

- Freelancers and agencies who need local prospects for outreach (web design, SEO, bookkeeping, cleaning, marketing services).
- Sales teams building a first-pass list of tradespeople, clinics, shops, hotels or restaurants in a city.
- Researchers and market analysts who want category counts and addresses for a region.
- Developers who need a clean JSON/CSV feed of businesses by category and location.

### What you get

For every business that has a name and at least one contact detail:

- Name, category, subcategory, address parts, city, postcode, country code
- Phone numbers (as written in OSM, normalised to `+49...` form when the number is international)
- Website, Facebook, Instagram, LinkedIn, X/Twitter when listed in OSM
- Email with a status: `verified_mx` (the domain accepts mail), `unverified`, or `none`, and where it was found (`osm` or `website:/contact`)
- Opening hours (OSM syntax), latitude/longitude, OpenStreetMap link
- Stable `id` such as `node/123456789` for deduplication across runs

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `searches` | array | required | Items like `{"query": "dentist", "location": "Berlin, Germany"}` or the string `"dentist in Berlin"` (also `"dentist \| Berlin"`). Up to 100 searches. |
| `maxResultsPerSearch` | integer | 100 | 1-2000 businesses saved per search. Only places with a name and a contact detail count. |
| `onlyWithWebsite` | boolean | false | Keep only places that list a website in OSM. |
| `findEmails` | boolean | true | Read the business website (homepage + up to 3 contact/imprint/about pages), extract emails and MX-check them. |
| `includeFailed` | boolean | false | Add a free `rowType: "error"` row for each search that failed (never charged). |

Categories: more than 100 common categories are mapped to OpenStreetMap tags (restaurant, cafe, dentist, doctor, pharmacy, lawyer, accountant, plumber, electrician, hairdresser, beauty salon, gym, hotel, car repair, bakery, supermarket, real estate, veterinary, school and many more) including synonyms in English, German, French, Spanish and Ukrainian. You can also type a raw OSM tag such as `amenity=dentist`. Unknown words fall back to a search in business names.

### Output example

```json
{
  "rowType": "business",
  "id": "node/1001",
  "name": "Zahnarztpraxis Dr. Müller",
  "category": "dentist",
  "subcategory": "dentist",
  "address": "Friedrichstraße 12, 10117, Berlin",
  "street": "Friedrichstraße",
  "houseNumber": "12",
  "city": "Berlin",
  "postcode": "10117",
  "country": "DE",
  "phone": "+49301234567",
  "phones": ["+49301234567", "+49307654321"],
  "website": "http://mueller-dental.example/",
  "email": "praxis@mueller-dental.example",
  "emailStatus": "verified_mx",
  "emailSource": "osm",
  "otherEmails": [],
  "facebook": "https://www.facebook.com/mueller.dental",
  "instagram": "https://www.instagram.com/mueller_dental",
  "linkedin": null,
  "twitter": null,
  "openingHours": "Mo-Fr 08:00-18:00",
  "lat": 52.520123,
  "lon": 13.404988,
  "osmUrl": "https://www.openstreetmap.org/node/1001",
  "searchQuery": "dentist in Berlin, Germany",
  "source": "OpenStreetMap (Overpass API)",
  "attribution": "© OpenStreetMap contributors (ODbL)",
  "scrapedAt": "2026-10-14T09:30:00.000Z"
}
```

Each run also writes an `OUTPUT` record (counts, charged events, skipped places with reasons, warnings, `stoppedReason`) and a short `RESULTS_MD` summary to the default key-value store.

### Pricing

Pay only for results (pay per event). No monthly rental.

| Event | Price | When it is charged |
|---|---|---|
| `business` | $0.004 | One business with a name and at least one contact field (phone, website, email or social link). |
| `verified-email` | $0.008 | Extra, only when an email was found for that business **and** its domain passed the MX check. |

So a business with an email costs $0.012, one without an email $0.004. 1,000 businesses cost between $4 and $12 depending on how many have a verified email. Apify adds volume discounts for paid plan tiers (Bronze, Silver, Gold). You are never charged for failed searches, places without contact data, duplicates, or anything beyond the maximum cost per run that you set. Free-plan users can try the actor with their free monthly credit.

### How it works

1. Each location is geocoded with OpenStreetMap Nominatim (1 request per second, identified user agent).
2. The category is translated to OSM tags and sent to the Overpass API as a query over the place's area. If a public Overpass server is busy (HTTP 429/504), the actor rotates to mirror servers with backoff; for very large areas that time out it falls back to smaller map tiles.
3. Places without a name are dropped; duplicates (the same OSM object found by several searches) are removed.
4. If `findEmails` is on, the business website is read politely: robots.txt is respected, one request per host at a time with a pause, small page-size limits, honest user agent. Only addresses on the business's own domain (or a free mailbox such as Gmail published on the page) are kept; web agency and widget addresses are ignored.
5. The email domain is checked for MX records. Only results that pass are billed as `verified-email`.
6. Progress is saved, so a restarted or migrated run never charges or outputs the same place twice.

### Limits and honest caveats

- **Coverage depends on OpenStreetMap.** Western and Central Europe is dense; other regions can be sparse. Many OSM places have no phone or website and are therefore skipped (and not charged).
- **Email coverage is partial.** Expect emails for a minority to roughly half of businesses that have a website, depending on country. Contact forms without a visible address, JavaScript-only sites, and sites that block robots yield nothing.
- `verified_mx` means the email's domain can receive mail. It does **not** prove that the mailbox exists or that the address is read by a person. We do not probe mail servers (no SMTP).
- Results are capped by Overpass limits; very large areas may be truncated (a warning is added to `OUTPUT`). Use a city or district rather than a whole country.
- Opening hours, phone formats and categories are community data and can be outdated or inconsistent.
- No reviews, ratings, photos or prices: that data does not exist in OpenStreetMap.

### Legal and ethical notes

- Business data from OpenStreetMap is licensed under the Open Database License (ODbL). If you publish or share the data or a derived database, you must credit "© OpenStreetMap contributors" and comply with ODbL (share-alike for derived databases). The `attribution` field is included in every row.
- Only publicly available business contact data is collected. Business email addresses may still be personal data under GDPR/ePrivacy rules in your jurisdiction. You are responsible for having a lawful basis, honouring opt-outs and following anti-spam laws (CAN-SPAM, GDPR/PECR, CASL) when you contact people.
- Website reading respects robots.txt and uses modest request rates. The actor uses the public Nominatim and Overpass services under their usage policies; for large commercial volumes consider your own mirrors (see FAQ).

### FAQ

**Is this a Google Maps scraper?** No. It uses OpenStreetMap only. It does not access Google data.

**Why did I get fewer results than I asked for?** OSM may simply not contain that many named places with contact data in the area. Check `OUTPUT.skipped` and `OUTPUT.warnings` in the key-value store.

**Do I pay for businesses without a phone, website or email?** No. They are skipped and not charged.

**How accurate are the emails?** They are public addresses read from the business's own pages; `verified_mx` only means the domain can receive mail. Expect some outdated or role addresses such as info@.

**Can I search a whole country?** You can, but public Overpass servers may truncate or time out. Split by city or region.

**Which languages are supported for categories?** English plus common German, French, Spanish and Ukrainian words. Others fall back to a name search, or use raw tags like `shop=bakery`.

**Can I use my own Overpass server?** The actor reads the `OVERPASS_URL`, `OVERPASS_MIRRORS` and `NOMINATIM_URL` environment variables; set them in the actor's environment variables for private mirrors.

**Why is a run slow?** Geocoding is limited to 1 request per second and websites are fetched politely. Turn off `findEmails` for faster runs.

### Use it via API

Replace `YOUR_USERNAME` and the token. The actor ID is `YOUR_USERNAME~local-business-leads`.

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~local-business-leads/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searches":[{"query":"dentist","location":"Berlin, Germany"}],"maxResultsPerSearch":50}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('YOUR_USERNAME/local-business-leads').call({
    searches: ['plumber in Hamburg, Germany'],
    maxResultsPerSearch: 100,
    onlyWithWebsite: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]);
```

```python
from apify_client import ApifyClient

client = ApifyClient("APIFY_TOKEN")
run = client.actor("YOUR_USERNAME/local-business-leads").call(run_input={
    "searches": [{"query": "bakery", "location": "Vienna, Austria"}],
    "maxResultsPerSearch": 100,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items))
```

### Integrations

Use the Apify integrations for Make, n8n, Zapier, Google Sheets, Airtable, Slack and webhooks: start the actor on a schedule, then send each dataset item to your CRM or spreadsheet. Export formats include JSON, CSV, Excel and XML. Typical flow: Schedule -> run actor -> filter `emailStatus = verified_mx` -> add rows to your CRM.

### Support

Found a category that does not map well, or a country with missing data? Open an issue on the actor page with the search you used and the expected result.

### More tools by the same author

- [Website Contact Extractor](https://apify.com/tiger19935/website-contact-extractor): Emails, phones and social links from any list of websites
- [B2B Email Verifier](https://apify.com/tiger19935/b2b-email-verifier): Verify business emails and guess address patterns
- [Domain to Company Enrichment](https://apify.com/tiger19935/domain-company-enrichment): Turn domains into company profiles
- [Decision-Maker Contacts](https://apify.com/tiger19935/decision-maker-contacts): Founders, CEOs and department heads from company websites

# Actor input Schema

## `searches` (type: `array`):

List of searches. Each item is an object {"query": "dentist", "location": "Berlin, Germany"} or a simple string like "dentist in Berlin" (also "dentist | Berlin"). The query is a business category or keyword (restaurant, dentist, plumber, hair salon, car repair, bakery ...; 100+ categories and many synonyms in English, German, French, Spanish, Ukrainian are mapped to OpenStreetMap tags; unknown words fall back to a name search). The location is any place name that OpenStreetMap can find: a city, district, region or country. Up to 100 searches per run. Use a specific city rather than a whole country for best results.

## `maxResultsPerSearch` (type: `integer`):

Maximum number of businesses saved for each search (1-2000). Only businesses with a name and at least one contact detail (phone, website, email or social link) count and are charged. Large cities can contain more places than OpenStreetMap lists with contact data; you may get fewer rows than requested.

## `onlyWithWebsite` (type: `boolean`):

When enabled, only places that have a website listed in OpenStreetMap are returned. Recommended if you want emails, because emails are found by reading the website.

## `findEmails` (type: `boolean`):

When enabled (default), the actor reads each business website (homepage plus up to 3 contact, imprint or about pages, respecting robots.txt), extracts public email addresses and checks that the email domain can receive mail (MX record). Emails listed in OpenStreetMap are always checked too. Switching this off makes runs faster but returns only emails that OpenStreetMap already contains.

## `includeFailed` (type: `boolean`):

When enabled, a free row (rowType 'error', never charged) is added to the dataset for each search that failed, for example an unknown location. The run summary in the OUTPUT record lists failures either way.

## Actor input object example

```json
{
  "searches": [
    {
      "query": "dentist",
      "location": "Monaco"
    }
  ],
  "maxResultsPerSearch": 5,
  "onlyWithWebsite": false,
  "findEmails": true,
  "includeFailed": false
}
```

# Actor output Schema

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

All result rows.

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

Counts, charged events and warnings.

# 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 = {
    "searches": [
        {
            "query": "dentist",
            "location": "Monaco"
        }
    ],
    "maxResultsPerSearch": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("tiger19935/local-business-leads").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 = {
    "searches": [{
            "query": "dentist",
            "location": "Monaco",
        }],
    "maxResultsPerSearch": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("tiger19935/local-business-leads").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 '{
  "searches": [
    {
      "query": "dentist",
      "location": "Monaco"
    }
  ],
  "maxResultsPerSearch": 5
}' |
apify call tiger19935/local-business-leads --silent --output-dataset

```

## MCP server setup

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

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/cfgDFiAJ375VJT1Tj/builds/MyjBWpY4bjyyARkFy/openapi.json
