local.ch Scraper — Swiss Business Directory Leads
Pricing
$1.00 / 1,000 business records
local.ch Scraper — Swiss Business Directory Leads
Scrape Swiss business leads from local.ch: company name, phone, mobile, email, website, street address, postal code, city and canton. Search by trade and place in German, French or Italian (Sanitär Zürich, Plomberie Genève). Business listings only — no private persons. $1 per 1,000 records.
Pricing
$1.00 / 1,000 business records
Rating
0.0
(0)
Developer
Per Schondell
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
The local.ch scraper that turns Switzerland's biggest business directory into a clean B2B lead list: company name, phone, mobile, email, website, street address, postcode, city and canton for any trade in any Swiss town — in German, French, Italian or English. Type Sanitär + Zürich, press Start, export Swiss business leads to JSON, CSV or Excel.
Business listings only. local.ch is Switzerland's phone book as well as its business directory — on a live "Sanitär Zürich" page local.ch's own counters read 339 results, 334 of them businesses. This actor emits only entries local.ch marks as a business; private persons cannot be produced and there is no setting that turns them on. See Business-only by design.
What can you do with it?
- Firmen in Zürich mit Telefonnummer finden — jede Zeile trägt die Telefonnummer im internationalen
+41-Format, direkt für CRM oder Dialer - Entreprises à Genève avec numéro de téléphone — cherchez
Plomberie+Genèveen français et recevez des fiches en français - Get Swiss business leads with email addresses — email comes straight out of the search result: measured 15 of 20 businesses on page 1 and 20 of 20 on page 5 of a Zürich search. No detail crawl, no extra cost
- Trovare aziende in Ticino — Italian searches (
Idraulico+Lugano) return Italian-language listings - Export local.ch search results to CSV or Excel — one clean row per business, stable columns, ready to import
- Build canton-by-canton prospect lists —
Treuhandin Zug,Garagein Bern,Fiduciairein Lausanne - Respect the Swiss "no advertising" marker automatically — every record carries
refusesAdvertising, so you can exclude the businesses that legally may not be cold-contacted (UWG art. 3 para. 1 lit. u). Measured: 14 of 20 businesses on the captured page carry it - Feed AI agents and pipelines — structured JSON via API, webhooks or the Apify MCP server
Pricing
$1 per 1,000 business records ($0.001 per record). You pay only for records actually delivered to your dataset — a failed or empty search costs nothing, because data is pushed to the dataset before it is charged.
| Scenario | Records | Run cost |
|---|---|---|
| Plumbers in Zürich | ~100 | ~$0.10 |
| One trade across the 20 largest Swiss cities | ~2,000 | ~$2.00 |
| Three trades x 4 language regions | ~1,200 | ~$1.20 |
There is no enriched tier and no per-company detail crawl: email, website, mobile, canton and coordinates are already in the search results, so everything is delivered at the single per-record price.
No proxy required. The default configuration uses no proxy at all — local.ch answered every probe over plain HTTP, with no browser and no challenge — so the only platform cost on top of the per-record price is a few cents of compute. This actor runs on plain HTTP: no headless browser, no residential proxy bill.
How to scrape local.ch (step by step)
- Click Try for free — you need a free Apify account.
- Pick the Language (
de,fr,it,en) your search terms are written in. - Enter one or more Search terms in that language (e.g.
Sanitär,Elektriker,Treuhand, orPlomberie,Électricien). - Enter Locations — Swiss cities, towns or postcodes (e.g.
Zürich,Bern,Genève,Lugano). At least one is required. - Optionally adjust Max results per search (default 100 per term x location) and the request delay. Leave Proxy configuration off — it is not needed.
- Click Start, then open the Dataset tab and export as JSON, CSV, Excel, HTML or XML.
Prefer pasting URLs? Put local.ch search URLs straight into Start URLs instead of search terms — e.g. https://www.local.ch/de/q/zuerich/sanitaer.
Input example
{"searchTerms": ["Sanitär", "Elektriker"],"locations": ["Zürich", "Winterthur"],"language": "de","maxResultsPerSearch": 100,"requestDelayMs": 1000}
Output example (one dataset record)
{"name": "A. Baltensperger AG","phone": "+41443665070","phones": ["+41443665070"],"mobile": null,"fax": "+41443665080","email": "info@baltensperger.ch","emails": ["info@baltensperger.ch"],"website": "http://www.baltensperger.ch","socialMediaUrls": ["https://www.facebook.com/A-Baltensperger-AG-326233344933861"],"address": "Langmauerstrasse 65","postalCode": "8006","city": "Zürich","canton": "ZH","latitude": 47.391818,"longitude": 8.542592,"categories": ["Haustechnik", "Sanitär", "Heizungen", "Lüftung", "Renovation", "Badezimmerrenovation", "Solartechnik Solaranlagen"],"reviewScore": 5,"reviewCount": 1,"refusesAdvertising": true,"cheNumber": null,"language": "de","isPremium": true,"profileUrl": "https://www.local.ch/de/d/zuerich/8006/haustechnik/a-baltensperger-ag-NBlHPNWpqBhH0KmssCHc7w","site": "local.ch","sourceUrl": "https://www.local.ch/de/q/zuerich/sanitaer","searchTerm": "Sanitär","location": "Zürich","scrapedAt": "2026-08-18T12:29:30+00:00"}
That is a real record, byte for byte, as this actor produced it from a live local.ch page — a unit test re-parses this very JSON block and diffs it against the parser, so it cannot drift. Note refusesAdvertising: true on this one: that business may not be sent unsolicited advertising.
Missing values are explicit null, so CSV and Excel exports always have the same columns. Not every business publishes an email — see the coverage numbers above.
Use it from code or AI agents
curl -X POST "https://api.apify.com/v2/acts/worktech~local-ch-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"searchTerms":["Sanitär"],"locations":["Zürich"],"language":"de","maxResultsPerSearch":100}'
The actor also works with n8n, Make, Zapier, webhooks and the Apify MCP server — its flat, structured output is designed to be consumed by AI agents directly.
Multilingual by design
Switzerland has four language regions and local.ch serves a different result set per language, not a translated one. This actor exposes that:
| Language | Example search | Example URL local.ch normalises to |
|---|---|---|
de | Sanitär + Zürich | /de/q/zuerich/sanitaer |
fr | Plomberie + Genève | /fr/q/geneve/plomberie |
it | Idraulico + Lugano | /it/q/lugano/idraulico |
en | Plumber + Zurich | /en/q/zurich/plumber |
Every record also carries the language local.ch itself assigned to that listing, so a mixed run stays sortable. And you never have to guess how a Swiss place name transliterates: the actor sends where=Zürich and local.ch answers with its own slug (zuerich), so umlauts, accents and bilingual town names resolve the way local.ch resolves them.
Business-only by design
local.ch indexes private individuals next to companies. Personal data is not what a lead-generation tool should be shipping, so the phone book is walled off in four independent layers:
- URL layer — every URL the actor builds is a trade + place search. A location-only search (which lists everyone in town) cannot be built, and a location-only URL in Start URLs is rejected before a single request is made.
- Page layer — any page local.ch marks as a phone-book search is discarded whole, not parsed.
- Record layer — only entries local.ch marks
BUSINESSare emitted. That is an allowlist: an unknown entry type is refused, not guessed at. An entry that arrives with no marker at all is additionally shape-tested against Swiss person-name rules, which cover hyphenated names (Anne-Marie Jensen), particles (von Arx,Da Silva),Mc/Macprefixes, titles (Dr. Hans Weber), block capitals (HANS WEBER), initials (Weber A.), the inverted form (Weber, Hans) and a bare surname (Meier). A declared legal form always wins, soAnne-Marie Jensen GmbHstays a company. - Evidence layer — every drop is counted and written to the run SUMMARY as
privatePersonEntriesDropped, present even when it is0.
An entry that arrives with no marker at all is decided in two steps: anything it publishes that only a business has — a website, a trade category or a CHE/UID number — keeps it, and the name-shape test decides only when there is no such evidence. That order matters: on a live French page a real plumbing business with a website and four plumbing categories was withheld purely because its name is two capitalised words. The trade-word vocabulary is plural-tolerant, so Alpes Sanitaires reads as a business exactly like Alpes Sanitaire does — a listing that publishes nothing at all is still judged on the whole of its name.
That last rule is deliberately asymmetric: refusing an unmarked bare name costs the occasional single-word brand — visible to you as a drop in the SUMMARY — while publishing it costs a private individual their privacy.
There is deliberately no includePersons input, and a test asserts that no input in the schema can re-enable them.
Sole traders that local.ch itself lists as businesses (e.g. Caseri Walter) are registered businesses and legitimate B2B leads — they are kept.
Why this scraper?
- Email and mobile at the base price — both are in the search result, so there is no detail-page crawl, no enrichment surcharge and no proxy blow-up. Measured coverage on live pages: 15/20 and 20/20 with an email.
- A real end-of-pagination signal — the walk stops on local.ch's own hit counter and its own
maxResultWindow, never on "the page looked full". And if local.ch ever ignores thepageparameter, the run fails loudly instead of re-emitting page 1 over and over — and charging you for each copy. - The Swiss compliance flag other scrapers drop —
refusesAdvertisingis published as a first-class field. - Genuinely multilingual — de/fr/it/en searches with the per-listing language on every row.
- No silent failures — a burned search is a named error in the run SUMMARY (with the status that burned it) and never kills the other searches; a charge failure is never swallowed; and you are never billed for records you did not receive, because records are pushed before they are charged. If local.ch renames the fields this scraper reads, the run fails loudly instead of handing you an empty dataset with a green tick.
- Honest accounting on a partial run — if a search burns halfway, the records it already delivered stay in the dataset and stay in the SUMMARY count, and the run is not marked failed. That matters because failed runs get retried: an under-reported run is how you end up paying twice for the same rows.
- Fast and cheap by construction — plain HTTP, no headless browser, one page at a time by default.
FAQ
Do I need an API key or a local.ch account? No. You need a free Apify account; the actor needs no credentials of any kind and only reads pages that are public to any visitor.
Can I export the leads to CSV or Excel? Yes. Every run produces a dataset downloadable as CSV, Excel, JSON, HTML or XML from the Apify Console, or via the API.
Do I get email addresses? Yes, whenever local.ch publishes one — it is part of the search result, so it costs nothing extra. Coverage is high in Swiss cities (measured 15/20 and 20/20 on captured pages) and lower in small villages.
Does it collect private persons? No, and it cannot. See Business-only by design.
What is refusesAdvertising?
Swiss directory entries can carry a marker meaning "no advertising, please". Under UWG art. 3 para. 1 lit. u it is unfair competition to send unsolicited advertising to those entries. The actor publishes the flag on every record so you can filter them out; the run SUMMARY also counts them.
Why is cheNumber usually null?
The Swiss company identifier (CHE-xxx.xxx.xxx) is not published on local.ch's search result pages — measured across 60 live entries, zero carried one. The field is filled when a listing does expose it and is otherwise null; it is never inferred or guessed from another source.
How many results can I get per search?
The default is 100 records (5 result pages) per search term x location — and that default is ours, not local.ch's. It was set after local.ch answered HTTP 403 on result pages 10, 17 and 18 during development; on later runs the same deep pages were served normally (page 17 returned 200 with 19 records), so the limit appears to vary rather than being fixed. Raise Max results per search — or set it to 0 — if you want deeper result sets, and watch the run SUMMARY: if the wall does reappear, the records already collected are kept and the search is listed under depthBlockedSearches, so a short result set is never mistaken for a complete one. Splitting a search by postcode or neighbouring town also works and is gentler on the site.
Which language should I use?
The one your search terms are in, matching the region: de for Zürich/Bern/Basel, fr for Genève/Lausanne/Neuchâtel, it for Lugano/Bellinzona. Searching German terms in the French region returns few hits — that is local.ch's behaviour, not the actor's.
Which proxy should I use?
None — that is the default and the cheapest option. local.ch does not challenge this scraper. Only turn a proxy on if failed searches show up in the run SUMMARY, and then pick Apify Proxy → RESIDENTIAL group, country CH, and raise the delay. Avoid a country-targeted datacenter proxy: on accounts without that entitlement it fails every request with ProxyError: 407 Proxy Authentication Required.
Is this legal / FADP / GDPR-compliant?
The actor collects publicly published business contact information — the same data any visitor to local.ch sees — and structurally excludes private-person records. You still need your own lawful basis for B2B prospecting, you must honour the refusesAdvertising marker and Swiss marketing law, and you are the data controller for what you do with the export. When in doubt, consult a lawyer.
Limitations
-
Coverage is whatever local.ch publishes — businesses without a listed email or website come back with
nullin those fields. -
100 records per search term x location by default — this is the actor's own cap, not a hard site limit: local.ch refused deep pages with HTTP 403 during development but has served them since. Raise it (or set
0) for deeper sets; if the wall reappears the run SUMMARY reports it underdepthBlockedSearches. -
A location is required — only local.ch's trade + place grammar is verified against the live site, so a nationwide sweep is refused rather than guessed at.
-
Search terms must match the chosen language — local.ch matches its own category vocabulary per language region.
-
Records are per listing, not per organisation — a company with several branches appears once per branch, which is usually what you want for local outreach.
-
cheNumberis usually null — local.ch does not publish UID numbers on search results. -
Single-word and connective-style brand names can be dropped when the site publishes no business marker — a listing called
Migros,Chez MarcelorZur Alten Post, with no legal form and no trade word, reads as a personal name to the business-only filter. Measured exposure on live captures is zero (all 80 captured entries carry local.ch's own BUSINESS marker, so the name rule is never consulted), and the usual directory forms (Restaurant zur Alten Post,Hotel Zum Goldenen Löwen,Coop Genossenschaft,Le Petit Chalet SA) are unaffected. It fails in the safe direction and every drop is counted in the run SUMMARY, so you can see exactly how many.
Status
v0.1 — 257 unit tests passing offline against pages captured from the live site, covering the RSC payload parser and its reference resolution, every dataset field, the business-only guards, pagination termination, the 403 depth wall, billing order, partial-run accounting and input validation. Twenty guards are additionally verified by mutation: each one was broken on purpose and the matching test was seen to fail. The captured pages in this repository were pseudonymised before they were committed — every private mobile number, every consumer mailbox and every personal name on a sole-trader listing was replaced, with the record counts left intact.