# Local.ch $1💰 Listings | URLs | Reviews Scraper (`abotapi/local-ch-scraper`) Actor

From $1/1K. Pull structured company data from local.ch, including name, address, GPS, phone, email, website, hours, ratings, social links, photos, and categories. Search by category and location or use URLs. Supports Deutsch, English, Français, and Italiano.

- **URL**: https://apify.com/abotapi/local-ch-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 24 total users, 3 monthly users, 92.0% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 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

## local.ch Scraper: Swiss Business Listings, Contacts & Reviews

local.ch Scraper turns local.ch, the Swiss business directory, into structured data: names, full addresses, GPS coordinates, phone and email, websites, opening hours, ratings, social profiles, attribute tags and photos. Search by category and location (city, postal code, canton, or all of Switzerland), or paste local.ch search and profile links directly. Works in Deutsch, English, Français and Italiano, then export to JSON, CSV or Excel, or read the results through the API.

### Why This Scraper?

- **Two ways in, one filtered lane out.** Search by category and location, or paste local.ch search/profile links. An optional review-only switch turns either path into one row per review comment instead of one row per company.
- **Four languages, one dataset shape.** Pages and category slugs resolve automatically in DE, EN, FR and IT, so `coiffeur` in French and `friseur` in German return the same normalized fields.
- **Detail-page enrichment by default.** Search-result cards alone only carry name, address and category, so the actor visits every profile page to add phone, email, GPS, opening hours, social links, attributes and photos.
- **Lead-gen filters that do not silently zero out.** Keep only rated, premium-listed, or contactable businesses; the actor automatically turns on detail-page fetching whenever one of those filters needs data that only lives on the profile page.
- **Built for recurring monitoring.** Incremental mode classifies each row against the previous run for the same search, URL, or review scope, and a search whose pages cannot be read is reported in the run log with a clear warning.
- **Full polymorphic contact data preserved.** Every raw contact and attribute object from local.ch is kept alongside the flattened fields, so nothing upstream adds gets lost before the dataset catches up.
- **Regional-fallback and postal-code guards.** Two opt-in toggles catch local.ch quietly padding a sparse search with unrelated regions, so a narrow query does not quietly return noise.

### Use Cases

- **Lead generation and sales prospecting:** pull phone, email and website for every business in a canton, filtered to premium-listed or contactable companies only.
- **Local SEO and directory audits:** check how a business's local.ch profile (categories, hours, photos, attributes) compares with its listings elsewhere.
- **Market research and competitor mapping:** sweep multiple categories across a canton or all of Switzerland to size a market.
- **Reputation monitoring:** collect review comments for a business or category and track sentiment over time.
- **Recurring lead monitoring:** schedule the actor with incremental mode to catch new, updated or removed businesses in a market segment.

### Data You Get

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

| Field | Example |
| --- | --- |
| `id` | `"aaaaaaaaaaaaaaaaaaaaaa"` |
| `name` / `subtitle` | `"Sample Café Zürich"` / `"Sample Café GmbH"` |
| `language` | `"de"` |
| `isPremium` | `true` |
| `mainCategory` / `mainCategorySlug` | `"Café"` / `"cafe"` |
| `categories` / `categorySlugs` | `["Café", "Tea room", "Catering"]` / `["cafe", "tea-room", "catering"]` |
| `address` | `"Sample Street 1, 8001 Zürich"` |
| `latitude` / `longitude` | `47.0000` / `8.0000` |
| `phone` / `mobile` | `"+41440000000"` / `"+41760000000"` |
| `email` / `website` | `"info@example.com"` / `"https://example.com/"` |
| `facebook` / `instagram` / `linkedin` | social profile URLs (when present on the profile) |
| `rating` / `ratingCount` / `reviewCount` | `4.5` / `120` / `60` |
| `reviews` | `[{ "title": "Great service", "text": "...", "author": "Reviewer", "date": "2026-01-01", "rating": 5 }]` (embedded, newest first, up to `maxReviewsPerListing` = 20 by default, when `includeReviews=true`) |
| `openingHours` | `"Mon: 09:00-18:00; Tue: 09:00-18:00; Wed: closed; ..."` (always lists all 7 days) |
| `openingHoursStructured` | `[{ "day": "MONDAY", "status": "open", "byArrangement": false, "intervals": [...] }, ...]` |
| `openingHoursByArrangement` | `false` (listing-level flag, `true` when any day is "by arrangement") |
| `description` | `"Sample description paragraph appears here when fetchDetails=true."` |
| `attributes` | `["Takeaway", "Delivery", "Vegetarian options"]` |
| `spokenLanguages` / `paymentMethods` | `["Deutsch", "Englisch"]` / `["Barzahlung", "TWINT", "Visa"]` (labels in the page language, when the profile lists them) |
| `refusesAdvertising` | `false` (listing output only; `true` when a contact carries local.ch's "no advertising" flag, `null` when the profile page could not be read) |
| `breadcrumb` | `["Home", "Food", "Café"]` |
| `logoUrl` / `imageUrls` | photo and logo URLs from the profile gallery |
| `contactsRaw` / `attributeGroupsRaw` | the full upstream polymorphic contacts and attribute-group objects, kept verbatim |
| `detailUrl` / `searchQuery` / `searchWhere` | `"https://www.local.ch/de/d/zurich/8001/cafe/sample-cafe-aaaaaaaaaaaaaaaaaaaaaa"` / `"cafe"` / `"zurich"` |

Every row also carries `street`, `zipCode`, `city`, `canton` and `country` as separate fields alongside the combined `address`, plus `entryType`, `schemaOrgType`, `favorited` and `professionalTitle` (the last one is set only for individually-listed professionals). When `reviewOnly` is turned on, each row is a single review comment instead of a business listing: `businessId`, `businessName`, `businessUrl`, `businessCity`, `businessZipCode`, `reviewId`, `reviewTitle`, `reviewText`, `reviewDate`, `reviewAuthor`, `reviewReply` and `reviewRating`, one row per review of that business (newest first, up to `maxReviewsPerListing` per business).

The dataset tab offers the same rows through several views: **Results** (everything), **Company overview** (core listing fields), **Contacts only** (phone/email/website/socials), **Review comments** (`reviewOnly` rows), **Incremental changes** (the `NEW`/`UPDATED`/`EXPIRED`/`REAPPEARED` rows an incremental run classified), and **Embedded reviews** (one row per review comment unwound out of each listing's `reviews` array when `includeReviews=true`). Pick whichever view matches what you need; every view reads the same underlying dataset.

### How to Use

1. Pick a start: fill in **Category** (or **Categories** for a multi-query sweep) and **Location** under Option 1, or set **Run mode** to "Paste local.ch URLs" (`mode: url`) and paste local.ch search or profile links under Option 2. Pasted links are only used when Run mode is "Paste local.ch URLs"; in the default Search mode the `urls` list is ignored.
2. Leave **Fetch detail pages** on (the default) for full contact and hours data, or turn on **Review-only output** to collect review comments instead of business listings.
3. Set any filters you need (rating, phone, email, website, premium-only) and cap the run with **Maximum rows** and **Maximum pages per search**.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by category in one city:**

```json
{
  "mode": "search",
  "category": "restaurant",
  "where": "zurich",
  "language": "de",
  "maxPages": 5,
  "fetchDetails": true
}
```

**Paste local.ch URLs (search and profile pages mixed):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.local.ch/de/s/restaurant/zurich",
    "https://www.local.ch/en/d/zurich/8001/tea-room/sample-name-aaaaaaaaaaaaaaaaaaaaaa"
  ],
  "maxPages": 3,
  "fetchDetails": true
}
```

**Default URL list:** in URL mode, leaving `urls` untouched (or omitting it in an API call) scrapes 12 distinct example URLs, one per genuinely different shape from the table below, sharing `maxListings` evenly between them. The input form is prefilled with those 12 plus two redirecting examples (the short `/d/<id>` link, which redirects to the full profile URL, and the bare `local.ch` host, which redirects to `www.local.ch`); those two are not in the untouched-default list because they would return the same rows as another example. Provide your own `urls` to replace it.

**Every local.ch URL shape URL mode accepts:**

| Shape | Example | Notes |
| --- | --- | --- |
| Classic search, category + location | `https://www.local.ch/de/s/restaurant/zurich` | Walks result pages 20 at a time; local.ch 307-redirects this to a `?rid=` tracking variant, followed automatically. |
| Classic search, category only | `https://www.local.ch/de/s/pizza` | Nationwide (no location segment). |
| Classic search, reversed segments | `https://www.local.ch/en/s/zurich/pizza` | local.ch resolves which segment is the place regardless of order, with the same result set either way. |
| Canonical SEO search (path form) | `https://www.local.ch/de/q/zuerich/restaurant` | local.ch's own share-link shape, `/<lang>/q/<location>/<category>`; note the reversed segment order vs. `/s/`. |
| Canonical SEO search (query form) | `https://www.local.ch/de/q?what=restaurant&where=zuerich` | Same search, `?what=`/`?where=` instead of path segments. |
| Paginated / sorted search | `https://www.local.ch/de/s/restaurant/zurich?page=2`, `...?sorting=rating` | A pasted `?page=N` starts the walk at that page (partial scope). |
| Postal-code / canton-code location | `https://www.local.ch/de/s/restaurant/8001`, `https://www.local.ch/de/s/restaurant/zh` | Location segment may be a 4-digit postal code or a canton code. |
| Bare host | `https://local.ch/de/s/restaurant/zurich` | 301-redirects to `www.local.ch`, followed automatically. |
| Profile / detail page | `https://www.local.ch/en/d/zurich/8001/tea-room/sample-name-aaaaaaaaaaaaaaaaaaaaaa` | Scrapes that one business. |
| Short profile link | `https://www.local.ch/de/d/aaaaaaaaaaaaaaaaaaaaaa` | local.ch redirects it to the full profile URL, followed automatically. |
| Profile contact card (and its sibling links) | `https://www.local.ch/de/entries/aaaaaaaaaaaaaaaaaaaaaa/vcard` | The "add to address book" link on every profile page (also `/opening_hours`, `/feedbacks`, `/qr`). Converted to that business's profile, so you get the full row. |
| Any search shape + `?sorting=` | `...?sorting=rating` | Honoured: accepts `relevance`, `alphanum`, `distance`, `rating` (case-insensitive); any other value is dropped with a log warning rather than sent through. `distance` lists the nearest businesses first from the URL's own `coordinates=<lat>,<lng>` (or the **Sort by distance from** input); with no point local.ch returns A-Z order, so the run logs a warning. |
| Any search shape + `?filter_facet=` / `?filter_tags=` / `?filter_open=` | `...?filter_facet=attr_cuisine:italian` | Honoured: the filters local.ch shows next to its results (cuisine, diets, amenities, accessibility, open now, bookable, ...) are applied on every page of the crawl. You can also type them in the `facetFilters` input. |
| Any search shape + bare `?facet=` / `?tags=` / `?open=` | `...?facet=attr_diets:glutenfree` | **Not applied** by local.ch (results unchanged); pasting one logs a loud warning instead of silently ignoring it. Use `filter_facet=` or the `facetFilters` input instead. |
| Category or city hub/index page | `https://www.local.ch/de/kategorien/...`, `https://www.local.ch/de/staedte/...` (+ `/categories`, `/cities`, `/villes`, `/categorie`, `/citta` in EN/FR/IT) | **Not a search result.** These are multi-level letter/category/city index pages that link out to many individual `/q/<location>/<category>` searches. Paste the `/q/...` link you actually want instead; the actor logs a warning and skips the URL rather than returning 0 rows silently. |

**Review comments only, discovered from a category search:**

```json
{
  "mode": "search",
  "reviewOnly": true,
  "category": "restaurant",
  "where": "zurich",
  "maxListings": 10
}
```

**Lead-gen filter: only premium-listed companies with both phone and email, in a postal code:**

```json
{
  "mode": "search",
  "category": "garage",
  "where": "8001",
  "language": "de",
  "matchQueryPostalCode": true,
  "premiumOnly": true,
  "withPhone": true,
  "withEmail": true,
  "maxListings": 500
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/local-ch-scraper").call(run_input={"mode": "search", "category": "restaurant", "where": "zurich"})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["name"], row["city"], row["phone"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/local-ch-scraper').call({ mode: 'search', category: 'restaurant', where: 'zurich' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every row already collected there, so you do not pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search, URL or review scope. Each row is classified `NEW`, `UPDATED` (with `changedFields`), or `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on). `EXPIRED` rows are only produced after a run proves it scanned the whole tracked scope end to end, not when Maximum pages/rows capped it, Resume was used, or a pasted URL started mid-pagination (`?page=2` or higher), and only with `emitExpired` on. `REAPPEARED` can only follow a row that was previously marked `EXPIRED`, so it needs that earlier complete-scan run with `emitExpired` on before it can ever appear. `stateKey` names or shares the stored state; leave it empty and the actor derives one automatically from the search/filter/review setup. `categories`/`categorySlugs`/`mainCategory`/`mainCategorySlug` never count as a change on their own; local.ch's search-result cards return a different (but equally valid) category set on repeated fetches, so list any other fields you don't want treated as changes in `ignoreFieldsForChanges` (checked against nested fields too, e.g. inside `reviews`). Ignored fields are still returned in the output. With incremental mode off, row shape is exactly as described above, with no extra fields.

### 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 row, 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 have not 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` (build from category + location) or `url` (use the `urls` list). |
| `reviewOnly` | boolean | `false` | When `true`, emit one row per review comment instead of listing rows. Works in both search and URL modes. |
| `category` | string | `restaurant` | Single category slug or label. Used in search mode when `queries` is empty. |
| `queries` | array | `[]` | Multiple categories to walk in search mode. Overrides `category` when set. |
| `where` | string | `zurich` | Location: city slug, 4-digit postal code, 2-letter canton code, `switzerland`, or empty. |
| `locations` | array | `[]` | Search mode only. Optional list of places (same forms as `where`). When set, every category is searched in every place listed here instead of just `where`. Leave empty to search one place as before. |
| `language` | string | `de` | `de`, `en`, `fr`, or `it`. Affects URL slugs and content language in search mode; URL mode reads the language directly from each pasted link. |
| `urls` | array | 12 distinct example URLs (classic, nationwide, `/q/` path and query forms, `?page=`, `?sorting=`, full profile link, FR/EN variants, postal-code and canton-code locations, profile contact-card link) | local.ch URLs: search (any shape in the table above) or profile pages. Used in URL mode. **With the field untouched (or an API call that omits it) URL mode now scrapes one example of each shape**, with `maxListings` split evenly across them. With `reviewOnly=true`, profile URLs scrape reviews directly and search URLs discover profiles first. |
| `maxPages` | integer | none (prefill: `0`) | Optional bound on result pages walked per category/URL (20 results each). Leave empty or `0` to walk every result page. |
| `maxListings` | integer | `20` | Total row cap (`0` = unlimited). Multi-query inputs split the cap evenly across queries. |
| `fetchDetails` | boolean | `true` | Visit each profile page for phone, email, full address, coordinates, opening hours, social links and attributes. |
| `includeReviews` | boolean | `false` | Listing output only. Embed review comments (newest first) inside each search or URL mode row. Ignored when `reviewOnly=true`. |
| `maxReviewsPerListing` | integer | `20` | Maximum reviews per business, up to 200. Listing output: reviews embedded per row, `0` = none. Review-only output: review rows per business, `0` = every review local.ch lists (the row cap still applies). A profile page shows only its 3 newest reviews, so asking for more makes one extra small request per business that has more. |
| `maxReviewRows` | integer | `10` | Review-only output only. Default review-row cap when `reviewOnly=true` and `maxListings` is explicitly set to `0`. |
| `maxConcurrency` | integer | `6` | No longer user-configurable. Detail-fetch concurrency is fixed internally; kept in the schema only so saved task inputs keep validating. |
| `resumeFromRunId` | string | none | Paste a previous run or dataset ID to skip rows it already collected and return only new ones. Continues ONE specific interrupted run; for recurring monitoring of the same scope, use `incrementalMode` instead. |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring of the same search/URL/review scope. The first run returns everything as `NEW`; later runs return only the rows that changed. |
| `stateKey` | string | none | Incremental mode only. Name a monitoring campaign to keep its state stable, or deliberately share it across runs. Leave empty to derive a key automatically from the search/filter/review setup. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) rows unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) rows tracked previously but no longer found, marked `EXPIRED`. Only produced after a run proves a complete scan (no cap hit, no resume, no pasted mid-pagination URL). |
| `ignoreFieldsForChanges` | string\[] | `[]` | Incremental mode only. Extra output field names to ignore when deciding whether a row is `UPDATED` (`categories`/`categorySlugs`/`mainCategory`/`mainCategorySlug` are always ignored). Matched at any nesting depth, so it also works for a field inside `reviews`, `contactsRaw`, `attributeGroupsRaw` or `openingHoursStructured`. |
| `ratedOnly` | boolean | `false` | Keep only rows that have an average rating. |
| `minRating` | integer | none | Keep only rows at or above this rating (0 to 5). |
| `withPhone` | boolean | `false` | Keep only rows with a phone number. This data only exists on the profile page, so turning this on also enables Fetch detail pages, regardless of that toggle. |
| `withEmail` | boolean | `false` | Keep only rows with an email address. Forces Fetch detail pages on for the same reason as `withPhone`. |
| `withWebsite` | boolean | `false` | Keep only rows with a website. Forces Fetch detail pages on for the same reason as `withPhone`. |
| `premiumOnly` | boolean | `false` | Keep only premium-listed companies. Forces Fetch detail pages on for the same reason as `withPhone`. |
| `matchQueryPostalCode` | boolean | `false` | Search mode only. When the location is a postal code, keep only rows whose postal code exactly matches the queried one, dropping the rest before any detail fetch. Guards against local.ch padding a sparse page with other regions. |
| `skipRegionalFallback` | boolean | `false` | Search mode only. When a query has zero exact matches, local.ch can silently fall back to a region-wide search and return unrelated rows. When on, the actor detects this and emits no output for that query. |
| `sort` | string | `relevance` | The order local.ch returns results in (search mode, and pasted search URLs without their own `sorting=`). `relevance` is local.ch's default; `alphanum`, `rating` and `distance` each widen the result set slightly to include nearby or loosely related businesses. `distance` lists the nearest businesses to **Sort by distance from** first; the run fails if that point is empty. |
| `distanceFrom` | string | none | Only used with `sort=distance`: the point to measure from, as `latitude,longitude` (e.g. `47.3769,8.5417` for Zurich main station). A pasted URL's own `coordinates=` value takes precedence. |
| `facetFilters` | array | `[]` | The same filters local.ch shows next to its search results (cuisine, diets, amenities, accessibility, open now, bookable, ...), applied by local.ch itself. Type a plain name (`italian`, `wifi`, `open now`) or the exact filter value (`attr_cuisine:italian`). Filters from different groups must all match; filters from the same group match any of them. An unmatched plain name skips that search with an explanatory error rather than returning unfiltered results. |
| `excludeRefusesAdvertising` | boolean | `false` | Listing output only. Drop businesses that marked a phone, mobile or fax number with local.ch's "no advertising" flag (output field `refusesAdvertising`), plus any business whose profile page could not be read. This data only exists on the profile page, so turning this on also enables Fetch detail pages. |
| `proxy` | object | `{ useApifyProxy: true, apifyProxyGroups: ["RESIDENTIAL"], apifyProxyCountry: "CH" }` | Connection settings. If the residential tier is not available on your plan, the actor automatically falls back to a standard connection, then to a backup connection pool, so the run still completes. |
| `mcpConnectors` | array | none | Optional: send a summary of each row to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "id": "aaaaaaaaaaaaaaaaaaaaaa",
  "name": "Sample Café Zürich",
  "subtitle": "Sample Café GmbH",
  "language": "de",
  "entryType": "COMPANY",
  "schemaOrgType": "Cafe",
  "isPremium": true,
  "favorited": false,
  "professionalTitle": null,
  "address": "Sample Street 1, 8001 Zürich",
  "street": "Sample Street 1",
  "zipCode": "8001",
  "city": "Zürich",
  "canton": "ZH",
  "country": "CH",
  "latitude": 47.0000,
  "longitude": 8.0000,
  "phone": "+41440000000",
  "phoneAlt": null,
  "mobile": "+41760000000",
  "fax": null,
  "email": "info@example.com",
  "emailAlt": null,
  "website": "https://example.com/",
  "whatsapp": "+41760000000",
  "facebook": "https://www.facebook.com/example",
  "instagram": "https://www.instagram.com/example",
  "linkedin": "https://www.linkedin.com/company/example",
  "twitter": null,
  "tiktok": null,
  "pinterest": null,
  "youtube": null,
  "xing": null,
  "foursquare": null,
  "mainCategory": "Café",
  "mainCategorySlug": "cafe",
  "categories": ["Café", "Tea room", "Catering"],
  "categorySlugs": ["cafe", "tea-room", "catering"],
  "rating": 4.5,
  "ratingCount": 120,
  "reviewCount": 60,
  "reviewsFetched": 0,
  "reviews": null,
  "openingHours": "Mon: 09:00-18:00; Tue: 09:00-18:00; Wed: closed; Thu: 09:00-18:00; Fri: 09:00-18:00; Sat: 10:00-17:00; Sun: closed",
  "openingHoursStructured": [
    { "day": "MONDAY", "status": "open", "byArrangement": false, "intervals": [{ "type": "OPEN", "start": "09:00", "end": "18:00" }] },
    { "day": "WEDNESDAY", "status": "closed", "byArrangement": false, "intervals": [] }
  ],
  "openingHoursByArrangement": false,
  "description": "Sample description paragraph appears here when fetchDetails=true.",
  "descriptionHtml": "<p>Sample description paragraph appears here when fetchDetails=true.</p>",
  "attributes": ["Takeaway", "Delivery", "Vegetarian options"],
  "spokenLanguages": ["Deutsch", "Englisch"],
  "paymentMethods": ["Barzahlung", "TWINT", "Visa"],
  "refusesAdvertising": false,
  "breadcrumb": ["Home", "Food", "Café"],
  "logoUrl": "https://images.services.local.ch/aaaa/logo.jpg",
  "imageUrls": [
    "https://images.services.local.ch/aaaa/photo-1.jpg",
    "https://images.services.local.ch/aaaa/photo-2.jpg"
  ],
  "contactsRaw": [
    { "__typename": "MobileContact", "value": "+41760000000", "displayValue": "076 000 00 00", "whatsapp": true },
    { "__typename": "EmailContact", "value": "info@example.com", "displayValue": "info@example.com" },
    { "__typename": "CustomerProvidedURLContact", "value": "https://example.com/", "displayValue": "example.com" },
    { "__typename": "SocialMediaContact", "value": "https://www.facebook.com/example", "socialMediaProvider": "FACEBOOK" }
  ],
  "additionalContactsRaw": null,
  "attributeGroupsRaw": [
    { "groupName": "Services", "attributes": [{ "id": "takeaway", "name": "Takeaway" }] }
  ],
  "detailUrl": "https://www.local.ch/de/d/zurich/8001/cafe/sample-cafe-zurich-aaaaaaaaaaaaaaaaaaaaaa",
  "searchQuery": "cafe",
  "searchWhere": "zurich"
}
```

### Plan Requirement

The default connection requests a residential-tier proxy scoped to Switzerland, which loaded every page shape in our checks. A datacenter connection also loads local.ch, but in our checks about half of datacenter exits were refused (the actor rotates and retries search pages, while profile pages may come back without detail fields), so residential is recommended. Large pages can stall on a shared standard connection. The actor observes whether that tier is actually granted for your account: if it is not (for example on an Apify free plan), it logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies." and continues on a backup connection; once that backup actually serves a page it logs "Served over the backup connection (the default connection was unavailable)." Some pages may still be unavailable on the backup. A run that could not load a single results page ends as a failed run with a status message instead of a silent empty success: "The proxy connection was refused for this account, so no page could be read..." when every connection was refused, "No results: residential proxies are unavailable on this run ... and the backup connection could not load the requested local.ch pages" on a free plan, or "Could not load any results page (the requests failed)..." otherwise. Only a run that loaded no page at all fails this way; if some pages load and others do not, the run completes with whatever was collected and logs a warning per failed page. local.ch error pages (HTTP 500, for example on the category and city index pages) are treated as a site error and are not retried. An explicit connection group or your own proxy URLs are always honored as given.

### FAQ

#### How much does it cost?

You pay per company or review row returned, plus a separate detail-enrichment charge for rows where a profile page was actually fetched (once per business in review-only output). Reviews embedded in a company row (`includeReviews`) add no extra charge. The **Pricing** tab shows the current rates. Use **Maximum rows** to cap the cost of any run.

#### Is it legal to scrape local.ch?

This actor collects only publicly available business directory data. You are responsible for how you use it: follow local.ch's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Contact details come from the businesses' own listings; handle personal data such as phone numbers and emails in line with the privacy law that applies to you.

#### Can I track only new or changed businesses on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each row is then classified `NEW`, `UPDATED`, or `UNCHANGED` against the previous run for the same search, URL or review scope, and unchanged rows are suppressed and not billed unless you turn `emitUnchanged` on. `EXPIRED` (and the `REAPPEARED` classification that can follow it) only appear once a run has proven it scanned the whole tracked scope, so a capped or resumed run keeps the previous state untouched instead.

#### Why did my `withPhone`, `withEmail`, `withWebsite` or `premiumOnly` filter return nothing?

Search-result cards never carry phone, email, website or premium status; only the profile page does. If **Fetch detail pages** is off, the actor automatically turns it on whenever one of these filters is set, so the filter narrows real data instead of dropping every row. This is logged as a visible warning, not a silent change.

#### Why did a search return fewer rows than expected?

Two opt-in guards can legitimately shrink a run: **Only listings matching the query postal code** drops off-target cards before local.ch pads a sparse page with other regions, and **Only listings from exact query matches** skips a query entirely when local.ch itself silently falls back to a region-wide search. Both default off, so a normal run is unaffected unless you turn them on.

#### Why did my run return zero rows?

If local.ch cannot be reached for one search among several, or for a later page, the actor logs a warning for that search (for example "page 1: fetch failed, stopping") and moves on, so check the run log before concluding the search has no results. If no results page at all could be loaded, the run ends as failed with a status message explaining why (see Plan Requirement) rather than succeeding with zero rows. Run it again in a few minutes, or use **Resume from run ID** to continue an interrupted run without re-collecting or re-charging rows you already have.

#### 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/tutti-ch-scraper"><b>Tutti.ch Scraper</b></a><br>Scrape listings from tutti.ch across all 23 categories, including vehicles, property...</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/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/herold-at-scraper"><b>Herold.at Scraper</b></a><br>From $0.80/1K. Scrape Herold.at business listings across Austria into clean JSON. Extract...</td></tr>
<tr><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><td>📇 <a href="https://apify.com/abotapi/bbb-org-scraper"><b>BBB Scraper</b></a><br>Scrape bbb.org (Better Business Bureau, USA &amp; Canada) business profiles: BBB rating...</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/local-ch-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`):

Choose where the actor should start. Search mode builds searches from category and location. URL mode uses pasted local.ch URLs.

## `reviewOnly` (type: `boolean`):

When ON, output one row per review comment instead of profile listing rows. Works in both search and URL modes.

## `category` (type: `string`):

Used in search mode. Examples: `restaurant`, `coiffeur`, `hairdresser`, `dentist`, `garage`, `hotel`.

## `queries` (type: `array`):

Used in search mode. Optional list of categories to search. When set, this overrides the single `category` field.

## `where` (type: `string`):

Used in search mode. Examples: city slug (`zurich`), postal code (`8001`), canton code (`zh`), or `switzerland`.

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

Search mode only. Optional list of places (same forms as Location: city slug, postal code, canton code, or `switzerland`). When set, every category is searched in every place listed here, and the single Location field is not used. Leave empty to search one Location as before.

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

Used for URLs and page content. local.ch supports German, English, French, and Italian.

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

Used in URL mode. Paste one or more local.ch URLs.
• Search URL (any shape below): walks result pages
• Profile URL: scrapes one profile page (`/<lang>/d/<city>/<zip>/<category>/<name>-<id>`, the short `/<lang>/d/<id>`, or a profile's own `/<lang>/entries/<id>/vcard` contact-card link)

Supported search shapes: `/<lang>/s/<category>/<location>`, `/<lang>/s/<category>` (nationwide), `/<lang>/q/<location>/<category>` (local.ch's own SEO link form), and `/<lang>/q?what=<category>&where=<location>`. A pasted `sorting=` and local.ch's own filter params (`filter_facet=`, `filter_tags=`, `filter_open=`) are honoured; other filter-like params (`facet=`, `tags=`, `open=`) are not applied by local.ch and are warned about in the log, never silently dropped. Category/city hub pages (`/kategorien/...`, `/staedte/...`) are not search results and are skipped with a warning — paste the `/q/...` link they link out to instead.

With `reviewOnly=true`, the actor outputs review comments instead of listing rows.

Leaving this field untouched (or omitting it in an API call) in URL mode scrapes a short default list of 12 distinct examples (one per genuinely different shape); `maxListings` is split evenly across them. The short `/d/<id>` profile link and the bare `local.ch` host are also pre-filled as editable examples, but they redirect to a page another default already covers, so they are not in the default list.

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

Optional bound on result pages walked per category/keyword/URL. Leave empty to walk every result page.

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

Maximum dataset rows to output. Listing rows when `reviewOnly=false`, review rows when `reviewOnly=true`.

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

When ON, enrich listing rows with profile-page fields like phone, email, full address, coordinates, opening hours, social links, and attributes.

## `includeReviews` (type: `boolean`):

Listing output only. Embed review comments (newest first, up to 'Maximum reviews per listing') inside each listing row. Ignored when `reviewOnly=true`.

## `maxReviewsPerListing` (type: `integer`):

Maximum reviews per business (default 20, up to 200). Listing output: reviews embedded in each listing row when 'Include review comments' is ON; 0 = none. Review-only output: review rows per business; 0 = every review local.ch lists (the 'Maximum review rows' cap still applies). A profile page shows only its 3 newest reviews, so asking for more makes one extra small request per business that has more.

## `maxReviewRows` (type: `integer`):

Review-only output only. Default review-row cap when `reviewOnly=true` and `maxListings=0`.

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

Deprecated and ignored. Detail-fetch concurrency is fixed internally at the reliable default; the input is kept only so saved task inputs keep validating.

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

Listing output only. Keep only listing rows that have an average rating.

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

Listing output only. Keep only listing rows at or above this rating.

## `withPhone` (type: `boolean`):

Listing output only. Keep only listing rows with a phone number. This data only exists on the profile page, so turning this on also enables Fetch detail pages, regardless of that toggle.

## `withEmail` (type: `boolean`):

Listing output only. Keep only listing rows with an email address. This data only exists on the profile page, so turning this on also enables Fetch detail pages, regardless of that toggle.

## `withWebsite` (type: `boolean`):

Listing output only. Keep only listing rows with a website. This data only exists on the profile page, so turning this on also enables Fetch detail pages, regardless of that toggle.

## `premiumOnly` (type: `boolean`):

Listing output only. Keep only premium-listed profiles. This data only exists on the profile page, so turning this on also enables Fetch detail pages, regardless of that toggle.

## `matchQueryPostalCode` (type: `boolean`):

Search mode only. When ON and the location is a postal code (e.g. `8001`), keep only results whose postal code exactly matches the queried one, and skip the rest before any detail-page fetch (saves proxy GB). local.ch's search is not strictly postal-code-bound — when few exact matches exist it pads the page with other regions — so this removes those off-target hits. Default OFF preserves current behaviour.

## `skipRegionalFallback` (type: `boolean`):

Search mode only. When a query has zero exact matches, local.ch silently falls back to a region-wide search and shows unrelated results (e.g. `psychologie` in `1346`). When ON, the actor detects this fallback and emits no output for that query — no detail-page fetch, no rows. Default OFF preserves current behaviour.

## `sort` (type: `string`):

The order local.ch returns search results in (search mode, and pasted search URLs that don't set their own `sorting=`). `relevance` is local.ch's default order. Choosing `alphanum` (A-Z), `rating` (best rated first) or `distance` makes local.ch widen the result set slightly and include some nearby or loosely related businesses: measured for restaurant/Zurich on 2026-10-06, 2,123 results became 2,596. `distance` lists the businesses nearest to the point in 'Sort by distance from' first; the run stops with an error if that point is empty, because local.ch would otherwise return A-Z order.

## `distanceFrom` (type: `string`):

Only used with Sort order `distance`: the point to measure distance from, as `latitude,longitude` in decimal degrees, e.g. `47.3769,8.5417` (Zurich main station) or `46.9511,7.4386` (Bern). Right-click a spot in any map app to copy its coordinates. A pasted search URL's own `coordinates=` value takes precedence.

## `facetFilters` (type: `array`):

The same filters local.ch shows next to its search results, applied by local.ch itself. Type a plain name in any site language or English (`italian`, `Glutenfrei`, `wifi`, `wheelchair_accessible_bathroom`, `open now`) or the exact filter value (`attr_cuisine:italian`, `attr_diets:glutenfree`, `entry:bookable`). Filters from different groups must all match (e.g. Italian AND gluten-free); filters from the same group match any of them (e.g. Italian OR Swiss). Plain names are looked up on the search's first page, which costs one extra page request per search. If a name matches no filter local.ch offers for that search, that search is skipped and an error explains why, so you never get unfiltered results by mistake. Filter params in a pasted URL (`filter_facet=`, `filter_tags=`, `filter_open=`) are applied as well.

## `excludeRefusesAdvertising` (type: `boolean`):

Listing output only. Drop businesses that have marked a phone, mobile or fax number with local.ch's 'no advertising' flag (the asterisk next to the number; output field `refusesAdvertising`). Businesses whose profile page could not be read are also dropped, because their flag is unknown. This flag only exists on the profile page, so turning this on also enables Fetch detail pages.

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

Proxy settings. Swiss residential is recommended — local.ch serves large pages and the shared datacenter pool can stall on them. If residential isn't on your plan, the actor automatically falls back to a datacenter and then a backup pool.

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

Optional: paste a previous run ID (or dataset ID) from this actor. Rows already collected there are skipped, so this run only returns new ones. Use this after an interrupted run, or to continue a large pull in another run. For recurring monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring of the same search/URL/review scope. The first run returns all matching rows as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED rows. Turn on "Emit unchanged" or "Emit expired" only when you also want those rows returned (and billed). State is kept separately per search/URL/filter/review setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from run ID above.

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL/filter/review setup — different setups then never mix state with each other.

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

Off by default. Turn on to also return rows that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

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

Off by default. Turn on to also return rows that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max pages/rows capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Output field names that should NOT make a row count as UPDATED. categories/categorySlugs/mainCategory/mainCategorySlug are always ignored, because local.ch's search-result cards return a different (but equally valid) category set on repeated fetches of the same business — the same fields are stable once a profile page is fetched (Fetch detail pages ON, the default). openStatus/opensAt/closesAt (they describe the moment of the fetch) and lastModified (local.ch's own edit timestamp) are always ignored too. Add other field names here (e.g. description, attributes) if changes to them are not relevant to you. Ignored fields are still returned in the output.

## `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",
  "reviewOnly": false,
  "category": "restaurant",
  "queries": [],
  "where": "zurich",
  "locations": [],
  "language": "de",
  "urls": [
    "https://www.local.ch/de/s/restaurant/zurich",
    "https://www.local.ch/de/s/pizza",
    "https://www.local.ch/de/q/zuerich/restaurant",
    "https://www.local.ch/de/q?what=restaurant&where=zuerich",
    "https://www.local.ch/de/s/restaurant/zurich?page=2",
    "https://www.local.ch/de/s/restaurant/zurich?sorting=rating",
    "https://www.local.ch/de/d/zuerich/8004/restaurant/restaurant-milano-rPs42JlyMK6zuDlCzLm1MA",
    "https://www.local.ch/de/d/rPs42JlyMK6zuDlCzLm1MA",
    "https://www.local.ch/fr/s/restaurant/geneve",
    "https://www.local.ch/en/q/zurich/restaurant",
    "https://www.local.ch/de/s/restaurant/8001",
    "https://www.local.ch/de/s/restaurant/zh",
    "https://local.ch/de/s/restaurant/zurich",
    "https://www.local.ch/de/entries/XlOM3DSqSoeHWjGtRZozEw/vcard"
  ],
  "maxPages": 0,
  "maxListings": 20,
  "fetchDetails": true,
  "includeReviews": false,
  "maxReviewsPerListing": 20,
  "maxReviewRows": 10,
  "maxConcurrency": 6,
  "ratedOnly": false,
  "withPhone": false,
  "withEmail": false,
  "withWebsite": false,
  "premiumOnly": false,
  "matchQueryPostalCode": false,
  "skipRegionalFallback": false,
  "sort": "relevance",
  "facetFilters": [],
  "excludeRefusesAdvertising": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CH"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

## `contacts` (type: `string`):

No description

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

No description

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

No description

## `embeddedReviews` (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",
    "urls": [
        "https://www.local.ch/de/s/restaurant/zurich",
        "https://www.local.ch/de/s/pizza",
        "https://www.local.ch/de/q/zuerich/restaurant",
        "https://www.local.ch/de/q?what=restaurant&where=zuerich",
        "https://www.local.ch/de/s/restaurant/zurich?page=2",
        "https://www.local.ch/de/s/restaurant/zurich?sorting=rating",
        "https://www.local.ch/de/d/zuerich/8004/restaurant/restaurant-milano-rPs42JlyMK6zuDlCzLm1MA",
        "https://www.local.ch/de/d/rPs42JlyMK6zuDlCzLm1MA",
        "https://www.local.ch/fr/s/restaurant/geneve",
        "https://www.local.ch/en/q/zurich/restaurant",
        "https://www.local.ch/de/s/restaurant/8001",
        "https://www.local.ch/de/s/restaurant/zh",
        "https://local.ch/de/s/restaurant/zurich",
        "https://www.local.ch/de/entries/XlOM3DSqSoeHWjGtRZozEw/vcard"
    ],
    "maxPages": 0,
    "maxListings": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "CH"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/local-ch-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",
    "urls": [
        "https://www.local.ch/de/s/restaurant/zurich",
        "https://www.local.ch/de/s/pizza",
        "https://www.local.ch/de/q/zuerich/restaurant",
        "https://www.local.ch/de/q?what=restaurant&where=zuerich",
        "https://www.local.ch/de/s/restaurant/zurich?page=2",
        "https://www.local.ch/de/s/restaurant/zurich?sorting=rating",
        "https://www.local.ch/de/d/zuerich/8004/restaurant/restaurant-milano-rPs42JlyMK6zuDlCzLm1MA",
        "https://www.local.ch/de/d/rPs42JlyMK6zuDlCzLm1MA",
        "https://www.local.ch/fr/s/restaurant/geneve",
        "https://www.local.ch/en/q/zurich/restaurant",
        "https://www.local.ch/de/s/restaurant/8001",
        "https://www.local.ch/de/s/restaurant/zh",
        "https://local.ch/de/s/restaurant/zurich",
        "https://www.local.ch/de/entries/XlOM3DSqSoeHWjGtRZozEw/vcard",
    ],
    "maxPages": 0,
    "maxListings": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "CH",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/local-ch-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",
  "urls": [
    "https://www.local.ch/de/s/restaurant/zurich",
    "https://www.local.ch/de/s/pizza",
    "https://www.local.ch/de/q/zuerich/restaurant",
    "https://www.local.ch/de/q?what=restaurant&where=zuerich",
    "https://www.local.ch/de/s/restaurant/zurich?page=2",
    "https://www.local.ch/de/s/restaurant/zurich?sorting=rating",
    "https://www.local.ch/de/d/zuerich/8004/restaurant/restaurant-milano-rPs42JlyMK6zuDlCzLm1MA",
    "https://www.local.ch/de/d/rPs42JlyMK6zuDlCzLm1MA",
    "https://www.local.ch/fr/s/restaurant/geneve",
    "https://www.local.ch/en/q/zurich/restaurant",
    "https://www.local.ch/de/s/restaurant/8001",
    "https://www.local.ch/de/s/restaurant/zh",
    "https://local.ch/de/s/restaurant/zurich",
    "https://www.local.ch/de/entries/XlOM3DSqSoeHWjGtRZozEw/vcard"
  ],
  "maxPages": 0,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CH"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/local-ch-scraper --silent --output-dataset

```

## MCP server setup

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