search.ch Scraper — Swiss Business & Phone Directory Leads avatar

search.ch Scraper — Swiss Business & Phone Directory Leads

Pricing

$1.50 / 1,000 business records

Go to Apify Store
search.ch Scraper — Swiss Business & Phone Directory Leads

search.ch Scraper — Swiss Business & Phone Directory Leads

Scrape Swiss business leads from search.ch (tel.search.ch): name, phone, street address, postal code, city, categories and website. Search by trade and place in German, French or Italian (Sanitär Zürich, Plomberie Genève). Business listings only — no private phone-book entries. $1.50 per 1,000.

Pricing

$1.50 / 1,000 business records

Rating

0.0

(0)

Developer

Per Schondell

Per Schondell

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

Categories

Share

The search.ch scraper (tel.search.ch) that turns Switzerland's national directory into a clean B2B lead list: company name, phone number, street address, postcode, city, categories and website 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. search.ch is the Swiss phone book: on a live "sanitär in zürich" search its own facet counter reads 143 business and 17 private entries. This scraper emits business entries only — private phone-book records 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 — die Telefonnummer steht auf jeder Zeile im internationalen +41-Format, direkt für CRM oder Dialer
  • Entreprises à Genève avec numéro de téléphone — cherchez Plomberie + Genève et exportez la liste en français
  • Trovare aziende in TicinoIdraulico + Lugano in italiano
  • Export search.ch / tel.search.ch results to CSV or Excel — one clean row per business, stable columns
  • Build canton-by-canton prospect listsTreuhand in Zug, Garage in Bern, Fiduciaire in Lausanne
  • Respect the Swiss "no advertising" star automatically — every record carries refusesAdvertising, so you can exclude the entries that legally may not be cold-contacted (UWG art. 3 para. 1 lit. u). Measured on a live page: 10 of 20 entries carry it
  • Join it with local.ch dataentryId is localsearch's own identifier, the same one local.ch uses, so records from both directories line up
  • Feed AI agents and pipelines — structured JSON via API, webhooks or the Apify MCP server

Pricing

$1.50 per 1,000 business records ($0.0015 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, and a record is never pushed (or charged) twice.

ScenarioRecordsRun cost
Plumbers in Zürich~160~$0.24
One trade across the 20 largest Swiss cities~2,500~$3.75
Three trades x 4 language regions~1,500~$2.25

No proxy required. The default configuration uses no proxy at all — search.ch answered every probe over plain HTTP, no browser and no challenge — so the only platform cost on top of the per-record price is a few cents of compute.

How to scrape search.ch (step by step)

  1. Click Try for free — you need a free Apify account.
  2. Pick the Language (de, fr, it, en) your search terms are written in.
  3. Enter one or more Search terms in that language (e.g. Sanitär, Elektriker, Treuhand).
  4. Enter Locations — Swiss cities, towns, postcodes or cantons (e.g. Zürich, Bern, Genève). At least one is required.
  5. Optionally adjust Max results per search (default 100 per term x location; 0 walks to search.ch's own last page) and the request delay.
  6. Click Start, then open the Dataset tab and export as JSON, CSV, Excel, HTML or XML.

Prefer pasting URLs? Put search.ch trade searches straight into Start URLs — e.g. https://search.ch/tel/?was=sanit%C3%A4r&wo=z%C3%BCrich.

Input example

{
"searchTerms": ["Sanitär", "Elektriker"],
"locations": ["Zürich", "Winterthur"],
"language": "de",
"maxResultsPerSearch": 200,
"requestDelayMs": 1000
}

Output example (one dataset record)

{
"name": "Wanner Sanitär GmbH",
"phone": "+41442727664",
"phones": ["+41442727664"],
"email": null,
"address": "Förrlibuckstrasse 110",
"postalCode": "8005",
"city": "Zürich",
"categories": ["Sanitäre Anlagen und Installationen"],
"website": null,
"description": null,
"refusesAdvertising": true,
"cheNumber": null,
"language": "de",
"entryId": "p3CJRqEsW4oMCwaFyR96sg",
"profileUrl": "https://search.ch/tel/zuerich/foerrlibuckstrasse-110/wanner-sanitaer-gmbh",
"site": "search.ch",
"sourceUrl": "https://search.ch/tel/?was=sanit%C3%A4r&wo=z%C3%BCrich",
"searchTerm": "Sanitär",
"location": "Zürich",
"scrapedAt": "2026-08-18T12:34:58+00:00"
}

Missing values are explicit null, so CSV and Excel exports always have the same columns.

Use it from code or AI agents

curl -X POST "https://api.apify.com/v2/acts/worktech~search-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.

Multilingual by design

search.ch publishes every result list in four languages, and this actor speaks all of them:

LanguageExample searchResult page search.ch serves
deSanitär + Zürich/tel/?was=sanitär&wo=zürich
frPlomberie + Genève/tel/Genève/Plomberie.fr.html
itIdraulico + Lugano/tel/Lugano/Idraulico.it.html
enPlumber + Zurich/tel/Zurich/Plumber.en.html

The language of each result page is written onto every record — read from the page search.ch actually served, not from your input — so a Start URL like https://search.ch/tel/Gen%C3%A8ve/Plomberie.fr.html produces records marked fr even when the run's language setting says something else. A mixed multi-language run stays sortable.

Business-only by design

search.ch is a phone book with businesses in it, not the other way round — so the private entries are walled off in three independent layers:

  1. URL layer — every URL the actor builds is a trade + place search. A lookup without a trade (a plain name or number search — the white pages) cannot be built, and such a URL in Start URLs is rejected before a single request is made.
  2. Record layer — an entry is emitted only when search.ch marks it Business. That is an allowlist: an unknown marker is refused, not guessed at. search.ch omits the marker entirely when an entry publishes no phone number (3 of 20 entries on the captured page — 15%), so those are shape-tested against Swiss person-name rules instead: hyphenated names (Anne-Marie Jensen), particles (von Arx, Da Silva), Mc/Mac prefixes, 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, so Anne-Marie Jensen GmbH stays a company. The 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.
  3. Evidence layer — every drop is counted and written to the run SUMMARY as privatePersonEntriesDropped, present even when it is 0.

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.

There is deliberately no includePersons input, and a test asserts that no input in the schema can re-enable them.

Why this scraper?

  • It handles search.ch's cumulative pagination correctly. On search.ch, pages=2 does not mean "page 2" — it re-serves entries 1-10 and adds 11-20. This actor uses that to fetch several pages per request (160 records in 4 requests instead of 16) and suppresses every repeated entry, so nothing is delivered — or charged — twice. The number suppressed is reported in the run SUMMARY.
  • A real end-of-pagination signal — the walk stops on search.ch's own data-maxpage, never on "the page looked full".
  • The Swiss compliance flag other scrapers droprefusesAdvertising comes from the entry's own "no advertising" star, not from a lookalike attribute that disagrees with it entry by entry.
  • No silent failures — if search.ch reports 160 hits and the parser can read none of them, the run fails loudly instead of handing you an empty dataset with a green tick. A burned search is a named error in the SUMMARY and never kills the other searches; a charge failure is never swallowed.
  • 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 request at a time by default.

FAQ

Do I need an API key or a search.ch account? No. You need a free Apify account; the actor needs no credentials and only reads pages that are public to any visitor.

Can I export the leads to CSV or Excel? Yes — CSV, Excel, JSON, HTML or XML, from the Apify Console or the API.

Do I get email addresses? No — search.ch does not publish email addresses on its result pages, so email is always null rather than a guess. If email matters, pair this with a directory that publishes it on the result page.

Does it collect private persons? No, and it cannot. See Business-only by design.

What is refusesAdvertising? Swiss directory entries can carry a star meaning "no advertising, please". Under UWG art. 3 para. 1 lit. u it is unfair competition to send unsolicited advertising to those entries. Every record carries the flag and the run SUMMARY counts them.

Why is cheNumber usually null? The Swiss company identifier (CHE-xxx.xxx.xxx) is not published on search.ch result entries — measured across the captured pages, none carried one. The field is filled if an entry does expose it and is null otherwise; it is never inferred.

How many results can I get per search? As many as search.ch itself lists: the walk follows its own data-maxpage (16 pages, 160 entries, for a Zürich plumbing search). Set Max results per search to 0 for everything, or leave the default of 100.

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, it for Lugano/Bellinzona.

Which proxy should I use? None — that is the default and the cheapest option. Only turn a proxy on if failed searches show up in the run SUMMARY, and then pick Apify Proxy → RESIDENTIAL group, country CH. Avoid a country-targeted datacenter proxy: without that entitlement every request fails with ProxyError: 407.

Is this legal / FADP / GDPR-compliant? The actor collects publicly published business contact information and structurally excludes private phone-book 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

  • No email addresses — search.ch does not publish them on result pages.

  • Websites only where published — measured 4 of 20 entries on a live page (search.ch shows the link mainly on paid entries).

  • A location is required — only search.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.

  • Records are per entry, not per organisation — a company with several branches appears once per branch.

  • Sponsored ad blocks are not scraped — only genuine directory entries are emitted.

  • Single-word and connective-style brand names can be dropped when the site publishes no business marker — a listing called Migros, Chez Marcel or Zur 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 (3 of 20 captured entries have no marker and all three survive), 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 — 242 unit tests passing offline against pages captured from the live site, covering entry parsing, the business-only guards, the cumulative-pagination handling and its terminator, per-page language detection, billing order, partial-run accounting and input validation. Twenty-one 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, in every format it appeared in, and every personal name on a sole-trader listing was replaced, with the record counts left intact.