# Herold.at Scraper - Austrian Businesses, Contacts & Reviews (`abotapi/herold-at-scraper`) Actor

From $0.80/1K. Scrape Herold.at business listings across Austria into clean JSON. Extract names, addresses, GPS, phone numbers, emails, websites, ratings, reviews, opening hours, payment methods, and founding dates by category, city, or region.

- **URL**: https://apify.com/abotapi/herold-at-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Lead generation, Automation, Agents
- **Features:** Limited permissions
- **Stats:** 14 total users, 0 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.12 / 1,000 results

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

## Herold.at Scraper: Austrian Business Listings, Contacts and Reviews

Herold.at Scraper turns Herold.at, Austria's largest business directory, into a clean structured dataset. Get company names, full addresses, phone numbers, emails, websites, ratings, reviews, opening hours, payment methods and founding dates for any category in any Austrian city or region. Build a search from categories and locations, or paste a Herold.at search URL directly, then export the results to JSON, CSV or Excel, or read them through the API.

### Why This Scraper?

- **Two ways to start.** Pick categories and locations from the input panel, or paste any Herold.at search URL straight from your browser.
- **Rich contact data on every listing, even without detail fetching.** Name, full address, phone, email, website, rating and verified status all come from the search results page alone.
- **Optional detail enrichment.** Turn on Fetch detail pages to add GPS coordinates, opening hours, payment methods, founding date and the full review list for each listing.
- **Multi-search batching.** Give it several categories and several locations and it searches every combination in one run.
- **Automatic forward pagination.** The actor walks result pages until it reaches the natural end of the search, or the limit you set, whichever comes first.
- **Built for recurring monitoring.** Incremental mode returns only new and changed listings on repeat runs, and a refused or empty response is retried automatically with a fresh session.
- **Post-fetch filters.** Keep only verified listings, only rated listings, or only listings above a minimum rating, without changing the underlying search.

### Use Cases

- **Local lead generation:** build call lists and email lists for a category and city, complete with phone numbers and websites.
- **CRM enrichment:** cross-check existing company records against Herold.at for updated addresses, phone numbers and ratings.
- **Market and competitor research:** measure how many businesses exist per category and region, and how they compare on ratings and reviews.
- **Directory and aggregator sites:** feed a local business directory or comparison site with structured, current listings.
- **Recurring monitoring:** track a category or region on a schedule and get notified only about new, changed or closed listings.

### Data You Get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `id` | `"00000"` (Herold.at listing ID) |
| `url` | `"https://www.herold.at/gelbe-seiten/wien/00000/sample-company/"` |
| `name` | `"Sample Company"` |
| `category` | `"restaurant"` |
| `regionSlug` | `"wien"` |
| `isVerified` | `true` ("Verifiziert" badge on the listing) |
| `streetAddress` | `"Sample Street 1"` |
| `postalCode` | `"1010"` |
| `addressLocality` | `"Wien"` |
| `addressRegion` | `"Wien"` |
| `addressCountry` | `"AT"` |
| `telephone` | `"+43 1 0000000"` |
| `email` | `"contact@example.com"` (when the company supplied one) |
| `website` | `"https://example.com"` (when the company supplied one) |
| `logoUrl` | `"https://images.herold.at/optimize?url=...&width=320"` |
| `ratingValue` | `4.8` (average rating, 1 to 5) |
| `ratingCount` | `42` |
| `branchCode` | `"00000"` |
| `latitude` | `48.0000` (requires Fetch detail pages) |
| `longitude` | `16.0000` (requires Fetch detail pages) |
| `foundingDate` | `"2020"` (requires Fetch detail pages) |
| `paymentAccepted` | `["Cash", "Card"]` (requires Fetch detail pages) |
| `openingHours` | `[{ "day": "Monday", "opens": "09:00", "closes": "18:00" }]` (requires Fetch detail pages) |
| `reviews` | `[{ "author": "Sample Reviewer", "body": "Sample review text.", "rating": 5 }]` (requires Fetch detail pages) |
| `scrapedAt` | `"2026-01-01T00:00:00.000Z"` |

Every listing also carries `imageCount`, `bestRating` and `worstRating` from the search results, whether or not detail pages are fetched. Detail-enriched listings additionally carry `description`, `reviewCount`, `breadcrumb`, `services` (tag chips such as cuisine or amenities) and `branchen` (this company's own category memberships on Herold.at). In Incremental mode, every record additionally carries `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`), `changedFields`, `firstSeenAt` and `lastSeenAt`.

### How to Use

1. Pick a **mode**: `search` (categories and locations) or `url` (paste a prepared Herold.at search link).
2. In search mode, fill in one or more category slugs and, optionally, one or more location slugs. Leave locations empty to search all of Austria for that category.
3. Turn on **Fetch detail pages** if you need GPS, opening hours, payment methods, founding date or reviews, and set **Max pages** or **Max listings** to control run size and cost.
4. Click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search mode, single category and location, fast:**

```json
{
  "mode": "search",
  "categories": ["restaurant"],
  "locations": ["wien"],
  "maxPages": 5,
  "maxListings": 100
}
```

**Search mode, multiple categories and locations, with detail enrichment:**

```json
{
  "mode": "search",
  "categories": ["elektriker", "installateur"],
  "locations": ["wien", "salzburg", "bregenz"],
  "fetchDetails": true,
  "maxPages": 3,
  "maxListings": 200
}
```

**Search mode, Austria-wide for a category, no location filter:**

```json
{
  "mode": "search",
  "categories": ["zahnarzt"],
  "locations": [],
  "maxPages": 10
}
```

**URL mode, paste a prepared search link:**

Behaviour change: in URL mode, leaving `urls` untouched now uses one example of each supported shape (it used to scrape nothing); with the default Max listings of 20 only the first example is read, so raise Max listings to cover them all. Supported shapes:

| Shape | Example |
|---|---|
| Category + location | `https://www.herold.at/gelbe-seiten/wien/restaurant/` |
| Austria-wide category | `https://www.herold.at/gelbe-seiten/restaurant/` |
| Paginated page | `https://www.herold.at/gelbe-seiten/wien/restaurant/seite/2/` |
| Region with umlaut | `https://www.herold.at/gelbe-seiten/k%C3%A4rnten/restaurant/` |
| Sub-category | `https://www.herold.at/gelbe-seiten/wien/bierlokale-pubs/` |
| Single company page (one record) | `https://www.herold.at/gelbe-seiten/wien/4F7QT/kfc/` |
| A-Z firm directory letter (follows its company links and its `/seite/N/` pages, limited by Max pages and Max listings) | `https://www.herold.at/gelbe-seiten/firmen-az/a/` |

`http://` and bare `herold.at` links are normalised to `https://www.herold.at`. `?seite=N` links are read from page N (via `/seite/N/`); herold.at itself ignores the query. `category` is empty (null) for single company pages and A-Z letter pages, because their URL carries no category. Category index pages (`/branchen-az/`) list no businesses and are skipped with a warning. Paths that do not exist on the site (for example a region without a category) return a not-found page and yield no rows; a company link whose page is gone or cannot be read is skipped with a warning and not billed. Search mode keeps umlauts in names (`Kärnten`, lower-cased).

**URL mode, two category links:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.herold.at/gelbe-seiten/wien/restaurant/",
    "https://www.herold.at/gelbe-seiten/salzburg/elektriker/"
  ],
  "maxPages": 5,
  "maxListings": 100
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/herold-at-scraper").call(run_input={"mode": "search", "categories": ["restaurant"], "locations": ["wien"]})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["name"], listing["telephone"], listing["website"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/herold-at-scraper').call({ mode: 'search', categories: ['restaurant'], locations: ['wien'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Finding category and location slugs

The simplest way to find a valid slug is to browse Herold.at yourself: open the gelbe-seiten section of herold.at, click into any category, and copy the slug from the URL path, for example `restaurant` or `elektriker`. Do the same for a location by opening a search result page and reading the first path segment, for example `wien`. Not every location and category combination has its own page: cities such as Graz, Linz, Innsbruck and Klagenfurt commonly return no results on a combined location and category URL, even though they appear in listing addresses. Leave **Locations** empty to search all of Austria for a category, then filter the dataset by `addressLocality` afterwards if you need one specific city.

#### Resume and recurring updates

`resumeFromRunId` and Incremental mode solve different problems.

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and this run skips every listing `id` already saved there, appending only the new ones.
- **Incremental mode** (`incrementalMode`) monitors the same search across separate scheduled runs. The actor keeps its own baseline in a private key-value store, keyed by **State key** or by an automatic hash of mode, categories, locations, urls, the post-fetch filters and Fetch detail pages, so changing Max listings or Max pages tomorrow still matches the same baseline. Every listing is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED`, `REAPPEARED` or `EXPIRED`.
  - `UNCHANGED` rows are not saved to the dataset unless you turn on **Include unchanged listings**.
  - `EXPIRED` is only produced once a run completes a full scan of the search, with no Max listings or Max pages cap cutting it short and no `resumeFromRunId`, and you turn on **Include expired listings**. A capped or resumed run cannot tell "gone" apart from "not reached yet", so it leaves the previous state untouched instead of guessing.
  - `REAPPEARED` only happens for a listing that a previous run already marked `EXPIRED`. If Include expired listings has never been turned on, a listing that temporarily drops out of the results and comes back later is reported as `UPDATED` or `UNCHANGED`, not `REAPPEARED`.
- Combining Incremental mode with `resumeFromRunId` is only supported to seed the very first incremental run from an existing full crawl. Once a baseline exists for a State key, the run fails with a clear message asking you to remove `resumeFromRunId` or choose a different State key.
- **What counts as a change.** A listing is `UPDATED` when a compared field changed; `changedFields` names them. `scrapedAt`, `position`, `logoUrl`, `primaryImage`, `imageCount` and the order of `reviews` are never compared (they stay in the output). Use **Ignore fields when detecting changes (incremental mode only)** (`ignoreFieldsForChanges`) to exclude more, for example `reviewCount` or the nested `reviews.datePublished`. Changing this list never produces a wave of `UPDATED` rows: the stored baseline is re-compared with the current list on every run.
- `EXPIRED` is only reported after a complete scan. A run that hit Max listings or Max pages, started from a URL beyond page 1 (`/seite/N/`), or used `resumeFromRunId` keeps the previous state untouched and reports nothing as expired.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape, the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify > Settings > Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you haven't authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (categories and locations) or `url` (paste a link). |
| `categories` | array | none (prefill: `["restaurant"]`) | Category URL slugs, for example `restaurant`, `elektriker`, `zahnarzt`. Required in search mode. |
| `locations` | array | none (prefill: `["wien"]`) | Austrian location slugs, for example `wien`, `salzburg`, `bregenz`. Empty means Austria-wide. |
| `urls` | array | one example per supported URL shape | Full Herold.at search URLs. Used only in url mode. |
| `verifiedOnly` | boolean | `false` | Keep only listings flagged "Verifiziert" (post-filter, applies in both modes). |
| `ratedOnly` | boolean | `false` | Keep only listings with at least one rating (post-filter). |
| `minRating` | integer | none | 0 to 5, drop listings below this average rating (post-filter); 0 means off, values outside 1 to 5 are ignored. |
| `maxPages` | integer | none (hard cap 1000) | Result pages to walk per search or URL. Leave empty to walk until the site's own end-of-results signal, a hard cap of 1000 pages, or sooner once Max listings is reached. |
| `maxListings` | integer | `20` | Total cap across all searches. Hard cap of 100000 per run; `0` means that cap, still bounded by Max pages. |
| `fetchDetails` | boolean | `false` | Also fetch each listing's detail page for GPS, opening hours, payment methods, founding date and reviews. Multiplies request count about 30x per result page. Each delivered business whose detail page is read is charged as one detail-page event on top of the per-result charge. |
| `maxReviews` | integer | `20` | With `fetchDetails` on, keeps at most this many reviews per listing (maximum 100, 0 = none); reviews are part of the normal result row and add no extra charge. |
| `resumeFromRunId` | string | none | Previous run ID or dataset ID to continue a large walk across separate runs. |
| `incrementalMode` | boolean | `false` | Recurring monitoring of the same search: classify every listing as NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED. |
| `stateKey` | string | none (auto-derived from the search scope) | Manually name the Incremental mode baseline. |
| `emitUnchanged` | boolean | `false` | Also save UNCHANGED listings to the dataset. |
| `emitExpired` | boolean | `false` | Also save an EXPIRED row for a previously tracked listing that no longer appears, once a run completes a full scan. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only: field names (nested like `reviews.datePublished`) that never count as a change. |
| `proxy` | object | none (prefill: Apify residential, country AT; this is also the runtime fallback when the field is left empty; datacenter also works) | Connection settings. |
| `mcpConnectors` | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

#### Output views

The dataset has four views: **Listings** (search-result fields), **Detail** (GPS, opening hours, payment methods, founding date, description), **Reviews** (one row per review, with the listing id, name and URL; the number of reviews per listing is set by `maxReviews`) and **Changes** (Incremental mode: `changeType`, `changedFields`, first and last seen). For users coming from other Herold.at scrapers, `companyName`, `companyDetailUrl`, `companyDescription`, `companyAddress` and `companyPhone` correspond to `name`, `url`, `description`, `streetAddress` with `postalCode` and `addressLocality`, and `telephone`; no extra fields are added, so existing Incremental baselines are not affected.

### Output Example

> Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "id": "00000",
  "url": "https://www.herold.at/gelbe-seiten/wien/00000/sample-company/",
  "name": "Sample Company",
  "category": "restaurant",
  "regionSlug": "wien",
  "position": 1,
  "isVerified": true,
  "breadcrumb": ["Home", "Gelbe Seiten", "Restaurant", "Wien"],
  "streetAddress": "Sample Street 1",
  "postalCode": "1010",
  "addressLocality": "Wien",
  "addressRegion": "Wien",
  "addressCountry": "AT",
  "latitude": 48.0000,
  "longitude": 16.0000,
  "telephone": "+43 1 0000000",
  "email": "contact@example.com",
  "website": "https://example.com",
  "logoUrl": "https://images.herold.at/optimize?url=...&width=320",
  "primaryImage": "https://images.herold.at/optimize?url=...&width=320",
  "imageCount": 1,
  "foundingDate": "2020",
  "branchCode": "00000",
  "paymentAccepted": ["Cash", "Card"],
  "openingHours": [
    { "day": "Monday", "opens": "09:00", "closes": "18:00" },
    { "day": "Tuesday", "opens": "09:00", "closes": "18:00" }
  ],
  "ratingValue": 4.8,
  "ratingCount": 42,
  "reviewCount": 12,
  "bestRating": 5,
  "worstRating": 1,
  "reviews": [
    { "author": "Sample Reviewer", "body": "Sample review text.", "rating": 5, "datePublished": "2026-01-01" }
  ],
  "description": "Sample seller description text appears here when fetchDetails is on.",
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Plan Requirement

No paid proxy is required. In testing, the plain Apify datacenter pool read every Herold.at page type this actor uses (search pages, paginated pages, company pages), with no blocks observed. The prefill is still residential with country AT, which gives the most stable routing into Austria; the proxy group is your cost and your choice, so switch to datacenter in the **Proxy configuration** field if you prefer lower cost. Apify free plans do not include residential proxies: if the chosen connection is unavailable, the run logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and carries on over the account's default pool, then notes "Served over the backup connection (the default connection was unavailable)." Each listing is given a fixed time budget per attempt and retried on a fresh session.

If nothing can be read, the run says why instead of returning a silent empty dataset: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required", or "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Herold pages", or "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later." A page that Herold.at itself reports as not found (for example a location and category pair with no page) is treated as an empty result, not a connection error.

### FAQ

#### How much does it cost?

You pay per listing returned. Turning on Fetch detail pages adds far more requests per page, so it also adds more cost per listing. The **Pricing** tab shows the current rates (a start fee plus a per-result fee); reviews, detail fields and the changes view add no extra charge. Use **Max listings** and **Max pages** to cap the cost of any run.

#### Is it legal to scrape Herold.at?

This actor collects only publicly listed business directory data. You are responsible for how you use it: follow Herold.at's terms and the laws that apply to you, including any rules on using business contact details for outreach, and get legal advice if you plan commercial redistribution.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated, reappeared and, if enabled, expired listings, and unchanged ones are not billed by default.

#### What is the difference between Resume and Incremental mode?

Resume (`resumeFromRunId`) continues one specific interrupted run so you don't pay to re-scrape what it already saved. Incremental mode instead monitors the same search across separate scheduled runs and reports only what changed since last time. They solve different problems and are normally used one at a time, see the "Resume and recurring updates" tip above for the exact rules on combining them.

#### Why did my run return zero listings for a city I know has businesses there?

Two common causes. First, not every location and category combination has its own page on Herold.at, some city and category pairs simply return no results. Leave **Locations** empty to search all of Austria, then filter the output by `addressLocality`. Second, a post-fetch filter such as **Rated businesses only** or **Minimum average rating** may be dropping every listing that was found, try running without those filters first to confirm.

#### Why did my run fail instead of returning an empty dataset?

A fully refused connection does not return a silent empty dataset: the run log states the reason (see Plan Requirement). The run genuinely fails only in three cases: a connection setting that cannot be set up at all, a `resumeFromRunId` that cannot be read, or combining Incremental mode with `resumeFromRunId` on a search that already has saved state.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more leads data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>📇 <a href="https://apify.com/abotapi/truelocal-com-au-scraper"><b>TrueLocal AU Directory Listings &amp; Reviews Scraper</b></a><br>Scrape TrueLocal.com.au business listings by keyword, location, or URL. Extract names...</td><td>📇 <a href="https://apify.com/abotapi/yellow-pages-au-scraper"><b>Yellow Pages AU</b></a><br>From $1/1K. Scrape business listings from Yellow Pages Australia by type and location...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/pagesjaunes-fr-scraper"><b>PagesJaunes FR</b></a><br>From $0.8/1K. Scrapes business listings from PagesJaunes.fr (French Yellow Pages)...</td><td>📇 <a href="https://apify.com/abotapi/yellow-nz-scraper"><b>Yellow Pages NZ</b></a><br>From $0.8/1K. Scrapes business listings from Yellow.co.nz (New Zealand Yellow Pages)...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/2gis-places-scraper"><b>2GIS</b></a><br>From $1/1K. Extract business and place data from 2GIS.com at scale. Get names, addresses...</td><td>📇 <a href="https://apify.com/abotapi/local-ch-scraper"><b>Local.ch</b></a><br>From $1/1K. Pull structured company data from local.ch, including name, address, GPS...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/herold-at-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search mode = pick categories + locations + filters below. URL mode = paste any herold.at URL refined in a browser (recommended for filters not exposed below).

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

Category URL slugs as they appear on herold.at, e.g. "restaurant", "elektriker", "installateur", "frisör", "zahnarzt", "rechtsanwalt". Open https://herold.at/gelbe-seiten/ → click any category → copy the slug from the URL path. The cartesian product of categories × locations is searched.

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

Austrian city/region URL slugs (verified working combos: "wien", "salzburg", "bregenz"). Note: herold.at only generates SEO pages for some city × category combinations; cities like graz, linz, innsbruck, klagenfurt return 404 on the combined URL even though they appear in result addresses. Leave empty to search Austria-wide for each category, which always works. Open a herold.at SERP and copy the location segment from the URL path to verify before running large jobs.

## `urls` (type: `array`):

One or more full herold.at URLs. Tip: open herold.at in your browser, navigate to a category (e.g. /gelbe-seiten/wien/restaurant/), then paste the resulting URL here. Multi-URL supported. Category slugs and Location slugs above are ignored, they only build a Search-mode URL. The post-fetch filters below (Verified/Rated/Minimum rating) still apply to whatever this URL returns. Pagination starts at the page in the URL and walks forward. Supported: category+location, Austria-wide category, /seite/N/ pages, region (Bundesland) and sub-category pages, a single company page, and A-Z firm directory letter pages. http:// and bare herold.at are normalised to https://www. Category index pages (branchen-az) list no businesses and are skipped with a warning.

## `verifiedOnly` (type: `boolean`):

Keep only listings flagged "Verifiziert" on herold.at. Applied after fetch as a client-side filter (the site does not expose a server-side URL filter for this), so it narrows whichever mode above fetched the listings.

## `ratedOnly` (type: `boolean`):

Keep only listings that have at least one rating. Applied after fetch as a client-side filter, so it narrows whichever mode above fetched the listings.

## `minRating` (type: `integer`):

Drop listings with an average rating below this value (1-5). Listings without a rating are also dropped. Applied after fetch as a client-side filter, so it narrows whichever mode above fetched the listings.

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

How many SERP pages to walk per category/location combination (or per pasted URL). Each page returns up to ~30 listings. Leave empty to walk every result page — the walk stops naturally once a page repeats listings already collected or herold.at signals no more pages, or sooner once Max listings is reached. Hard cap of 1000 pages per search or URL, applied even when empty.

## `maxListings` (type: `integer`):

Stop after this many listings across all searches. Defaults to 20 so a run stays small unless you raise it. Hard cap of 100000 per run; 0 means that cap (still bounded by Max pages).

## `fetchDetails` (type: `boolean`):

When ON, also fetch each listing's detail page for GPS coordinates, opening hours, payment methods, founding date, full review list, description, and breadcrumb. SERP cards already carry name, full street address, postal code, city, region, telephone, email, website, rating, and verified status, so most use cases work fine with this OFF. Multiplies request count ~30× per SERP page when ON. Default OFF. Each delivered business whose detail page is read is charged as one detail-page event on top of the per-result charge; businesses whose detail page is refused, and businesses incremental mode suppresses as unchanged, are not charged.

## `maxReviews` (type: `integer`):

With Fetch detail pages ON, keep at most this many reviews per listing (newest as shown on the page). Default 20, maximum 100; 0 = no reviews. The review count field still shows the site total.

## `proxy` (type: `object`):

Works with any Apify proxy group, including datacenter. An Austrian (AT) exit gives the most stable routing; the default is residential AT, change the group or country to suit your cost.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by listing id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run. Distinct from Incremental mode below — this continues one specific interrupted run, not recurring monitoring.

## `incrementalMode` (type: `boolean`):

When ON, this actor remembers the previous run of the SAME search (see State key) and classifies each listing as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED. A listing is UPDATED when any compared field changed (contact data, address, rating, review count/content, opening hours, services, description, ...); the changed field names are listed in changedFields. Never compared (kept in the output, but never cause UPDATED): scrapedAt, position (search ranking), logoUrl, primaryImage, imageCount (image CDN URLs), and review order; add more under 'Ignore fields when detecting changes'. UNCHANGED and EXPIRED rows are not saved to the dataset unless enabled below — this is what keeps a scheduled recurring run cheap. EXPIRED is only detected after a complete scan (never when a limit was hit, a start URL begins after page 1, or Resume is used). Default OFF (every run returns every listing, as today). Combining with Resume from a previous run above is only safe on the very first incremental run (no baseline yet) — once a baseline exists, remove Resume from a previous run or set a different State key.

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

Manually name the incremental baseline for this search, e.g. "wien-restaurants". Leave empty to auto-derive one from mode + categories/locations/urls + the post-fetch filters + Fetch detail pages — two runs with the exact same search settings then share a baseline automatically. Set this explicitly if you want to run several DIFFERENT search configs against the SAME named baseline, or keep configs that happen to match fully separate.

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

When ON, incremental mode also saves listings with no detected change (changeType="UNCHANGED") to the dataset, instead of only NEW/UPDATED/REAPPEARED. This returns — and bills for — every listing every run, defeating the point of incremental mode for most use cases. Default OFF.

## `emitExpired` (type: `boolean`):

When ON, incremental mode also saves a row (changeType="EXPIRED") for each previously-tracked listing that no longer appears, once this run reaches the natural end of the search results (skipped on a capped, resumed, or otherwise incomplete run — those cannot tell "gone" apart from "not reached yet"). This returns — and bills for — an extra row per disappeared listing. Default OFF.

## `ignoreFieldsForChanges` (type: `array`):

Output field names that must NOT count as a change in incremental mode, e.g. description or reviewCount. Nested fields use a dot path, e.g. reviews.datePublished. The fields stay in the output; they just never make a listing UPDATED or appear in changedFields. Image URLs (logoUrl, primaryImage, imageCount), scrapedAt and position are always ignored. Changing this list never causes a wave of UPDATED rows.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "categories": [
    "restaurant"
  ],
  "locations": [
    "wien"
  ],
  "urls": [
    "https://www.herold.at/gelbe-seiten/wien/restaurant/",
    "https://www.herold.at/gelbe-seiten/restaurant/",
    "https://www.herold.at/gelbe-seiten/wien/restaurant/seite/2/",
    "https://www.herold.at/gelbe-seiten/k%C3%A4rnten/restaurant/",
    "https://www.herold.at/gelbe-seiten/wien/bierlokale-pubs/",
    "https://www.herold.at/gelbe-seiten/wien/4F7QT/kfc/",
    "https://www.herold.at/gelbe-seiten/firmen-az/a/"
  ],
  "verifiedOnly": false,
  "ratedOnly": false,
  "maxListings": 20,
  "fetchDetails": false,
  "maxReviews": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AT"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "categories": [
        "restaurant"
    ],
    "locations": [
        "wien"
    ],
    "urls": [
        "https://www.herold.at/gelbe-seiten/wien/restaurant/",
        "https://www.herold.at/gelbe-seiten/restaurant/",
        "https://www.herold.at/gelbe-seiten/wien/restaurant/seite/2/",
        "https://www.herold.at/gelbe-seiten/k%C3%A4rnten/restaurant/",
        "https://www.herold.at/gelbe-seiten/wien/bierlokale-pubs/",
        "https://www.herold.at/gelbe-seiten/wien/4F7QT/kfc/",
        "https://www.herold.at/gelbe-seiten/firmen-az/a/"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "AT"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/herold-at-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 = {
    "mode": "search",
    "categories": ["restaurant"],
    "locations": ["wien"],
    "urls": [
        "https://www.herold.at/gelbe-seiten/wien/restaurant/",
        "https://www.herold.at/gelbe-seiten/restaurant/",
        "https://www.herold.at/gelbe-seiten/wien/restaurant/seite/2/",
        "https://www.herold.at/gelbe-seiten/k%C3%A4rnten/restaurant/",
        "https://www.herold.at/gelbe-seiten/wien/bierlokale-pubs/",
        "https://www.herold.at/gelbe-seiten/wien/4F7QT/kfc/",
        "https://www.herold.at/gelbe-seiten/firmen-az/a/",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "AT",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/herold-at-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 '{
  "mode": "search",
  "categories": [
    "restaurant"
  ],
  "locations": [
    "wien"
  ],
  "urls": [
    "https://www.herold.at/gelbe-seiten/wien/restaurant/",
    "https://www.herold.at/gelbe-seiten/restaurant/",
    "https://www.herold.at/gelbe-seiten/wien/restaurant/seite/2/",
    "https://www.herold.at/gelbe-seiten/k%C3%A4rnten/restaurant/",
    "https://www.herold.at/gelbe-seiten/wien/bierlokale-pubs/",
    "https://www.herold.at/gelbe-seiten/wien/4F7QT/kfc/",
    "https://www.herold.at/gelbe-seiten/firmen-az/a/"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AT"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/herold-at-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/herold-at-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/M5l0zBCZV6G7l9czg/builds/FIhCmdpbebDdWRQpg/openapi.json
