Tutti.ch Scraper - Listings, Prices, Sellers & Details avatar

Tutti.ch Scraper - Listings, Prices, Sellers & Details

Pricing

from $1.00 / 1,000 results

Go to Apify Store
Tutti.ch Scraper - Listings, Prices, Sellers & Details

Tutti.ch Scraper - Listings, Prices, Sellers & Details

Scrape listings from tutti.ch across all 23 categories, including vehicles, property, electronics, furniture, fashion, and jobs. Search by keyword, category, city, or use tutti.ch URLs. Returns price, location, canton, seller details, images, and optional GPS, phone, photos, and attributes.

Pricing

from $1.00 / 1,000 results

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

19

Total users

3

Monthly active users

6 hours ago

Last modified

Share

Tutti.ch Scraper: Swiss Classifieds Listings, Pricing and Seller Data

Tutti.ch Scraper turns Switzerland's largest classifieds and marketplace site into structured data. Search by keyword, category and city, or paste tutti.ch search and category links directly, across all 23 categories (vehicles, property, electronics, furniture, fashion, jobs and more). Every listing comes back as one flat JSON record with the formatted and numeric price, full canton and location data, and seller details, in German, French or Italian. Turn on detail mode to add GPS coordinates, every photo, the seller's account ID, member since date, phone number when published and structured attributes, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

Why This Scraper?

  • Two ways to start. Search by keyword, category and city, or paste any tutti.ch search or category URL, multiple links in one run.
  • 23 categories. Vehicles, property, electronics, furniture, fashion, jobs and more, plus a free-text keyword filter and min/max price filters in search mode.
  • Numeric pricing. Every listing carries both the formatted price string and a parsed numeric value (priceValue), so you can filter and sort without parsing text yourself.
  • Full location and seller data. City, postcode, canton code and canton name on every listing, plus seller name and private vs professional status.
  • Optional detail enrichment. One setting adds GPS coordinates, every photo, the seller's account ID and member since date, phone when published, street address and structured attributes.
  • Three site editions. German, French and Italian, matched automatically to a pasted URL or chosen explicitly in search mode.
  • Built for long, resumable pulls. Point a new run at a previous run's ID to skip listings you already collected, and an in-progress run checkpoints itself automatically against platform interruptions.

Use Cases

  • Classifieds market research: track price trends and listing volume for a product category or city over time.
  • Price monitoring: watch specific keywords or categories for new or repriced listings.
  • Property and vehicle sourcing: pull real estate or vehicle listings with location and price filters to feed a search tool or alert.
  • Lead generation: collect seller and listing details, with detail mode on, for outreach tools, subject to tutti.ch's own terms.
  • Data science and analytics: build historical datasets of Swiss classifieds pricing and category activity.

Data You Get

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

FieldExample
listingId"00000001"
title"Sample bike for sale"
description"Full listing text appears here."
price / priceValue"499.-" / 499
currency"CHF"
categoryId"bicycles"
categoryLabel / parentCategoryId"Velos" / "sportsOutdoors" (detail mode only)
locationName / postcode"Sample City" / "8000"
canton / cantonName"ZH" / "Zürich"
latitude / longitude47.0000 / 8.0000 (detail mode only)
street"Sample Street 1" (detail mode only)
sellerName / sellerType"Sample Seller" / "private"
sellerAccountId / memberSince"0000000000000000000" / "2020-01-01T00:00:00Z" (detail mode only)
phone"+41 79 000 00 00" (detail mode only, and only when the seller published one)
highlightedfalse
imageUrl / imageCount"https://c.tutti.ch/big/0000000000.jpg" / 3
imagesone-item array of imageUrl by default, every photo in detail mode
propertiesstructured attributes such as size or condition (detail mode only)
timestamp"2026-01-01T00:00:00+02:00"
language"de"
url"https://www.tutti.ch/de/vi/zuerich/sport-outdoor/velos/sample/00000001"

categoryLabel, parentCategoryId, parentCategoryLabel, latitude, longitude, street, sellerAccountId, sellerLocation, sellerProfileUrl, memberSince, phone, externalUrl and properties are only populated when fetchDetails is turned on. description, price and priceValue are populated from the search result itself and stay populated either way, though fetchDetails can refine the price when tutti.ch's detail page carries a more precise numeric value.

How to Use

  1. Pick a mode: search (keyword, category and city) or url (paste tutti.ch links).
  2. Fill in the fields for that mode, or paste your URLs, and set sorting and site language as needed.
  3. Turn on fetchDetails if you need GPS, all photos, phone, street or structured attributes, and set maxPages and maxListings to control run size and cost.
  4. Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.

Basic keyword search:

{
"mode": "search",
"keyword": "velo",
"maxPages": 3,
"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "CH" }
}

Category in specific cities, price filtered:

{
"mode": "search",
"category": "vehicles",
"locations": ["zurich", "bern"],
"minPrice": 5000,
"maxPrice": 20000,
"sortBy": "price_asc",
"maxListings": 100
}

Paste tutti.ch URLs (multiple supported):

{
"mode": "url",
"urls": [
"https://www.tutti.ch/de/q/marktplatz/Ak8CrgIWcA",
"https://www.tutti.ch/de/q/velos/Ak8CoYmljeWNsZXOUwMDAwA"
],
"maxPages": 4
}

Full detail enrichment:

{
"mode": "search",
"keyword": "sofa",
"category": "household",
"fetchDetails": true,
"maxListings": 100
}

Supported URL shapes (URL mode)

Link shapeExample pathResult
Marketplace-wide search/de/q/marktplatz/...search results, 30 listings per page
Keyword or category search/de/q/velos/...search results
Paginated and sorted page/de/q/velos-fahrraeder/...?page=2&sorting=cheapestthat page, then the following pages
Search limited to a canton/de/q/velos-bern/...search results for that canton
French and Italian editions/fr/q/..., /it/q/...search results in that language
Bare search root/de/qall listings
Listing, id only/de/vi/83641798one listing
Listing, long form/de/vi/zuerich/.../80329196one listing
Listing, French edition/fr/vi/80442365one listing
Listing, short or wrong slug/de/vi/specialized-hotrock/83638829resolved by its id to one listing
Search without language/q/velos/...treated as the German edition
Apex or mobile hosttutti.ch/de/vi/83638829normalised, then as above

Seller pages (/seller?id=...) and category landing pages (/de/cat/...) cannot be read and are skipped with a warning; paste a /q/ search link or use search mode with Category instead. Leaving the URLs field untouched in URL mode scrapes one example of each distinct shape above (links without a language and apex or mobile host links are normalised to a shape already in the list, so they have no separate example); earlier versions used a single example link, so replace the list with your own links to scrape only those.

Output views and new fields

The dataset has three views: Overview (the main listing columns), Detail (adds the detail-mode columns: GPS, street, seller account, all photos, properties) and Changes since last run (incremental mode: changeType, changedFields, first and last seen). Newer fields are email, category_path and scraped_at, next to the Max listing age (hours) filter (maxAgeHours) and the Relevance sort (sortBy = relevant). Existing fields keep their names and types.

Billing

You are charged per run start and per listing saved to the dataset. With fetchDetails on, the detail-enrichment event is charged once for each listing whose detail page was fetched, right after that listing is saved. Listings that are filtered out, deduplicated, skipped or failed are never charged. See the Pricing tab for current rates.

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/tutti-ch-scraper").call(run_input={"mode": "search", "keyword": "velo", "maxListings": 50})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
print(listing["title"], listing["price"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/tutti-ch-scraper').call({ mode: 'search', keyword: 'velo', maxListings: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

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

Resuming a large pull

For an unbounded pull (maxPages left empty, maxListings set to 0), two things protect a long run:

  • resumeFromRunId lets you continue in a brand new run: paste the ID of a previous run, or its dataset, and this run skips every listingId already saved there, appending only new listings. An ID that doesn't match a run or dataset you can read fails the run immediately with a clear reason, instead of silently re-scraping everything.
  • A checkpoint is saved automatically as the run progresses. If the run is interrupted by an Apify platform migration, or you resurrect a failed run, it picks back up from where it left off in the same run, without re-saving or re-charging for listings it already collected. No input is needed for this, it's automatic.

Recurring runs: incremental mode

For a schedule that should only return what changed, turn on incrementalMode (off by default, so existing runs behave exactly as before). The actor remembers the listings it has seen for that search and labels every row it returns with changeType (NEW, UPDATED, REAPPEARED, plus UNCHANGED and EXPIRED when you ask for them), changedFields, firstSeenAt and lastSeenAt. The first run returns everything as NEW; later runs return only new, changed and reappeared listings. Listings that did not change are not returned, so they are not billed. The dataset view Changes since last run shows these rows.

  • What counts as a change: any field other than the ignored ones. A new price, title, description, images, seller, location or property makes the listing UPDATED, and changedFields names what moved. By default scraped_at, timestamp (the posted or renewed time) and highlighted (the promoted flag) are ignored, because they change without any change to the ad itself; they are still returned in every row. The order of the property list is also ignored. Add your own with ignoreFieldsForChanges: a name without a dot matches that field at any depth (for example text inside properties), a dotted name matches from the top of the row. Changing this list never causes a one-time wave of UPDATED rows.
  • State: kept per search (mode, keyword, category, locations, price range, language, detail setting, or the pasted URLs), never per maxListings, maxPages or the maximum listing age. Set stateKey to name a campaign yourself.
  • emitUnchanged (off) also returns listings that did not change, marked UNCHANGED. emitExpired (off) also returns listings that are no longer found, marked EXPIRED. Those rows are delivered and billed like any other row and count toward maxListings: any beyond it are reported on a later run. EXPIRED is only produced after a run that read the whole search to its last page: it is skipped when maxListings or maxPages stopped the walk, for a pasted single listing, a link that starts after page 1, a maximum listing age, Resume, or a page or detail page that failed to load.
  • With fetchDetails: a listing whose detail page could not be loaded is left out of that run (and not charged) and picked up on the next one, instead of being reported as changed.
  • Charges: the same events as a normal run. A row that is not returned is not charged.
  • Not combined with Resume: if a state already exists for the search, a run with resumeFromRunId fails with a clear message. Use a different stateKey to start a separate campaign.

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

ParameterTypeDefaultDescription
modestringsearchsearch builds a URL from keyword, category and city. url takes tutti.ch links you paste.
keywordstringnone (prefill: "velo")Free-text search term. Search mode only.
categorystringallRestrict to one of 23 tutti.ch categories. Search mode only.
locationsarraynone (example: ["zurich"])City slugs, e.g. zurich, bern, geneve. Leave empty to search all of Switzerland. Search mode only.
minPriceintegernoneKeep listings priced at or above this value; listings with no price are kept. Search mode only, results from a pasted URL are not price filtered.
maxPriceintegernoneKeep listings priced at or below this value; listings with no price are kept. Search mode only, results from a pasted URL are not price filtered.
urlsarraynine example links, one per supported shapetutti.ch search or category URLs (/de/q/..., /fr/q/..., /it/q/..., including ?page=N and ?sorting=) and single listing URLs (/de/vi/..., with or without the long path). Apex and mobile hosts are normalised. Seller pages are not supported and are skipped with a warning. URL mode only. The search-mode fields above and language are ignored here; sortBy still applies from the page after the pasted one onward.
sortBystringnewestnewest, oldest, price_asc, price_desc or relevant (tutti.ch relevance order, meaningful with a keyword). Applies in both search mode and URL mode.
languagestringdeSite edition: de, fr or it. Search mode only, in URL mode the language comes from the pasted URL and the page itself.
fetchDetailsbooleanfalseAdds GPS, all images, phone, seller ID, member since date, street and properties. One extra request per listing.
maxPagesinteger0 (unlimited)Result pages per search, 30 listings each. Left empty or 0, the actor walks every result page up to tutti.ch's own ceiling around page 100. When a category is set in search mode and maxListings is not 0, the actor walks at least 40 pages (up to 100) so its own client-side category filtering can still fill the listing target; use maxListings to bound those runs.
maxAgeHoursintegeremptyKeep only listings posted within the last N hours. With sortBy = newest the walk stops once a whole page is older than the limit.
maxListingsinteger20Hard cap on total listings across all searches. 0 means no limit, still bounded by maxPages.
proxyobjectApify Residential, country CHConnection settings.
maxResidentialPercentinteger100Soft cap on the residential share of the run before switching to a cheaper connection type. 100 means never downgrade on this signal.
residentialBudgetMbinteger0 (no limit)Residential bandwidth budget for the run, in megabytes.
resumeFromRunIdstringnonePrevious run ID or dataset ID to continue a full-catalogue walk across separate runs. Already-collected listings, matched by listing id, are skipped.
incrementalModebooleanfalseReturn only NEW, UPDATED and REAPPEARED listings on recurring runs, with changeType, changedFields, firstSeenAt, lastSeenAt.
stateKeystringderived from the searchOptional name for the monitoring campaign (incremental mode only).
emitUnchangedbooleanfalseAlso return unchanged listings, marked UNCHANGED (incremental mode only; billed like any row).
emitExpiredbooleanfalseAlso return listings no longer found, marked EXPIRED, after a complete walk only (incremental mode only; billed like any row).
ignoreFieldsForChangesarrayemptyExtra fields that never make a listing UPDATED (incremental mode only). Always ignored: scraped_at, timestamp, highlighted.
mcpConnectorsarraynoneOptional: send a summary of each record to apps you authorized under Integrations.
notionParentPageUrlstringnoneNotion connector only: page under which item pages are created.
maxNotifyListingsinteger50Cap 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 listing. This shows a default run (fetchDetails off); the fields noted above as detail mode only are null here and are filled in when fetchDetails is true.

{
"listingId": "00000001",
"title": "Sample listing title",
"description": "Full listing text appears here.",
"price": "499.-",
"priceValue": 499,
"currency": "CHF",
"categoryId": "bicycles",
"categoryLabel": null,
"parentCategoryId": null,
"parentCategoryLabel": null,
"locationName": "Sample City",
"postcode": "8000",
"canton": "ZH",
"cantonName": "Zürich",
"latitude": null,
"longitude": null,
"street": null,
"sellerName": "Sample Seller",
"sellerType": "private",
"sellerAccountId": null,
"sellerLogoUrl": "https://c.tutti.ch/img/0000000000.jpg",
"sellerLocation": null,
"sellerProfileUrl": null,
"sellerSubscriptionClass": null,
"sellerBadgeUrl": null,
"memberSince": null,
"phone": null,
"externalUrl": null,
"highlighted": false,
"formattedSource": null,
"imageUrl": "https://c.tutti.ch/big/0000000000.jpg",
"imageCount": 3,
"images": ["https://c.tutti.ch/big/0000000000.jpg"],
"properties": [],
"timestamp": "2026-01-01T00:00:00+02:00",
"language": "de",
"url": "https://www.tutti.ch/de/vi/zuerich/sport-outdoor/velos/sample/00000001"
}

Plan Requirement

tutti.ch refuses datacenter connections: they get a "Security check" page (HTTP 403) with no listing data, so the datacenter tier cannot be used. Only Swiss residential connections load listings, which means a paid Apify plan with residential proxy access is required. The default Proxy setting, Apify Residential with country CH, is already set up for this. If your plan does not include residential proxies, the log shows this warning and the actor continues on a backup connection, where some pages may still be unavailable:

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. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs.

When a request is served over the backup connection, the log says "Served over the backup connection (the default connection was unavailable)." You can also paste your own proxy URLs in the proxy field. For heavy or frequent runs, maxResidentialPercent and residentialBudgetMb cap residential usage.

If no results page could be loaded, the run fails with one of these messages instead of finishing with an empty dataset:

  • The proxy connection was refused for this account (your plan has no proxy access, or your own proxy credentials are invalid).
  • No results: residential proxies are unavailable on this run (free plan) and the backup connection could not load the requested tutti.ch pages.
  • Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search.

FAQ

How much does it cost?

You pay to start a run and per listing returned; detail mode adds the detail-enrichment event, charged once per listing whose detail page was fetched, right after that listing is saved to the dataset. Listings that are filtered out or not saved are never charged for details. The Pricing tab on the actor page shows the current rates. Use Max listings and Max pages to cap the size, and cost, of any run.

This actor collects only publicly available classifieds listings. You are responsible for how you use the data: follow tutti.ch's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution, particularly for seller contact details collected in detail mode.

Can I get only new listings on a schedule?

Yes. Turn on incrementalMode and schedule the actor from the Schedules tab with the same search each time. The first run returns every listing as NEW; later runs return only NEW, UPDATED and REAPPEARED listings, each with changedFields. See "Recurring runs: incremental mode" above for what counts as a change and when EXPIRED is reported. To continue one interrupted large pull instead, use resumeFromRunId.

Why are my keyword, category or price filters ignored?

URL mode with the field untouched. Running url mode without changing the URLs field scrapes one example of each distinct supported link shape: marketplace-wide search, keyword and category search, a paginated and sorted page, a canton-constrained search, French and Italian searches, the bare search root, and listing pages (id only, long form, and French). Links without a language and apex or mobile host links are normalised to a shape already in the list, so they have no separate example. Replace the list with your own links to scrape only those. Each listing link produces exactly one listing; seller pages (/seller?id=...) cannot be read and are skipped with a warning in the log.

Locations. In search mode, locations are city or canton names (Zürich, Genève, Bern, Kanton Wallis), in German, French, Italian or English spelling. A name tutti.ch does not recognise is reported as an error in the log and skipped.

Those fields, and site language, only apply in search mode. In url mode, the pasted tutti.ch link already carries its own search constraints and language, so the actor fetches it as given. sortBy is the one exception: it still applies to every page after the one you pasted.

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

A run stops with a failed status for an invalid resumeFromRunId: an ID that doesn't match a run or dataset your Apify account can read fails immediately with a clear reason, instead of quietly re-scraping everything. If no results page can be loaded at all (see Plan Requirement), the run also fails with a clear reason; if some pages loaded first, you keep whatever was collected. Re-running usually helps, since a fresh run gets a new session, and in URL mode it's worth double-checking the pasted link is a valid tutti.ch search or category page.

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 classifieds data?

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

🏷️ Kijiji.ca Scraper
Scrape Kijiji.ca listings across property, vehicles, jobs, electronics, furniture...
🏷️ Leboncoin FR
Pull public leboncoin.fr listings across cars, property, jobs, electronics, furniture...
🏷️ Mudah.my Cars, Property, Jobs & Marketplace Scraper
Scrape Mudah.my listings across every category: cars, motorcycles, property, mobiles...
🏷️ Kleinanzeigen.de Scraper
Scrape Kleinanzeigen.de ads by keyword, category, location, price, seller type, or search...
🏷️ Gumtree AU Scraper
Scrape gumtree.com.au classifieds across every vertical: for-sale goods, motors, real...
🚗 Turo Scraper
Collect Turo vehicles by location and trip dates or listing URLs. Get make, model, year...

👉 Browse all abotapi scrapers

💬 Support

  • 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. 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.