# Tutti.ch Scraper - Listings, Prices, Sellers & Details (`abotapi/tutti-ch-scraper`) Actor

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.

- **URL**: https://apify.com/abotapi/tutti-ch-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 22 total users, 2 monthly users, 95.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## 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

## 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.

| Field | Example |
| --- | --- |
| `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` / `longitude` | `47.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) |
| `highlighted` | `false` |
| `imageUrl` / `imageCount` | `"https://c.tutti.ch/big/0000000000.jpg"` / `3` |
| `images` | one-item array of `imageUrl` by default, every photo in detail mode |
| `properties` | structured 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:**

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

**Category in specific cities, price filtered:**

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

**Paste tutti.ch URLs (multiple supported):**

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

**Full detail enrichment:**

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

#### Supported URL shapes (URL mode)

| Link shape | Example path | Result |
| --- | --- | --- |
| 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=cheapest` | that 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/q` | all listings |
| Listing, id only | `/de/vi/83641798` | one listing |
| Listing, long form | `/de/vi/zuerich/.../80329196` | one listing |
| Listing, French edition | `/fr/vi/80442365` | one listing |
| Listing, short or wrong slug | `/de/vi/specialized-hotrock/83638829` | resolved by its id to one listing |
| Search without language | `/q/velos/...` | treated as the German edition |
| Apex or mobile host | `tutti.ch/de/vi/83638829` | normalised, 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:

```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:

```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

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` builds a URL from keyword, category and city. `url` takes tutti.ch links you paste. |
| `keyword` | string | none (prefill: `"velo"`) | Free-text search term. Search mode only. |
| `category` | string | `all` | Restrict to one of 23 tutti.ch categories. Search mode only. |
| `locations` | array | none (example: `["zurich"]`) | City slugs, e.g. `zurich`, `bern`, `geneve`. Leave empty to search all of Switzerland. Search mode only. |
| `minPrice` | integer | none | Keep 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. |
| `maxPrice` | integer | none | Keep 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. |
| `urls` | array | nine example links, one per supported shape | tutti.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. |
| `sortBy` | string | `newest` | `newest`, `oldest`, `price_asc`, `price_desc` or `relevant` (tutti.ch relevance order, meaningful with a keyword). Applies in both search mode and URL mode. |
| `language` | string | `de` | Site edition: `de`, `fr` or `it`. Search mode only, in URL mode the language comes from the pasted URL and the page itself. |
| `fetchDetails` | boolean | `false` | Adds GPS, all images, phone, seller ID, member since date, street and properties. One extra request per listing. |
| `maxPages` | integer | `0` (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. |
| `maxAgeHours` | integer | empty | Keep only listings posted within the last N hours. With `sortBy` = `newest` the walk stops once a whole page is older than the limit. |
| `maxListings` | integer | `20` | Hard cap on total listings across all searches. `0` means no limit, still bounded by `maxPages`. |
| `proxy` | object | Apify Residential, country `CH` | Connection settings. |
| `maxResidentialPercent` | integer | `100` | Soft cap on the residential share of the run before switching to a cheaper connection type. `100` means never downgrade on this signal. |
| `residentialBudgetMb` | integer | `0` (no limit) | Residential bandwidth budget for the run, in megabytes. |
| `resumeFromRunId` | string | none | Previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Already-collected listings, matched by listing id, are skipped. |
| `incrementalMode` | boolean | `false` | Return only NEW, UPDATED and REAPPEARED listings on recurring runs, with `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`. |
| `stateKey` | string | derived from the search | Optional name for the monitoring campaign (incremental mode only). |
| `emitUnchanged` | boolean | `false` | Also return unchanged listings, marked `UNCHANGED` (incremental mode only; billed like any row). |
| `emitExpired` | boolean | `false` | Also return listings no longer found, marked `EXPIRED`, after a complete walk only (incremental mode only; billed like any row). |
| `ignoreFieldsForChanges` | array | empty | Extra fields that never make a listing `UPDATED` (incremental mode only). Always ignored: `scraped_at`, `timestamp`, `highlighted`. |
| `mcpConnectors` | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which 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 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`.

```json
{
  "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.

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

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:

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/kijiji-scraper"><b>Kijiji.ca Scraper</b></a><br>Scrape Kijiji.ca listings across property, vehicles, jobs, electronics, furniture...</td><td>🏷️ <a href="https://apify.com/abotapi/leboncoin-fr-scraper"><b>Leboncoin FR</b></a><br>Pull public leboncoin.fr listings across cars, property, jobs, electronics, furniture...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/mudah-my-scraper"><b>Mudah.my Cars, Property, Jobs &amp; Marketplace Scraper</b></a><br>Scrape Mudah.my listings across every category: cars, motorcycles, property, mobiles...</td><td>🏷️ <a href="https://apify.com/abotapi/kleinanzeigen-de-scraper"><b>Kleinanzeigen.de Scraper</b></a><br>Scrape Kleinanzeigen.de ads by keyword, category, location, price, seller type, or search...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/gumtree-au-scraper"><b>Gumtree AU Scraper</b></a><br>Scrape gumtree.com.au classifieds across every vertical: for-sale goods, motors, real...</td><td>🚗 <a href="https://apify.com/abotapi/turo-scraper"><b>Turo Scraper</b></a><br>Collect Turo vehicles by location and trip dates or listing URLs. Get make, model, year...</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/tutti-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`):

How to start the scrape. 'search' builds listing URLs from a keyword, category and locations. 'url' takes tutti.ch search or category URLs you paste.

## `keyword` (type: `string`):

Free-text search term, e.g. 'velo', 'iphone', 'wohnung'. Leave empty to browse a whole category or the entire marketplace. Search mode only.

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

Restrict the search to one tutti.ch category. 'All categories' searches the entire marketplace. Search mode only.

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

Swiss cities or cantons by name, e.g. 'Zürich', 'Genève', 'Bern', 'Basel', 'Lausanne', 'Luzern', 'Winterthur', or 'Kanton Wallis'. German, French, Italian and English spellings are accepted. A location that returns no listings is reported as an error in the log. Leave empty to search all of Switzerland. For precise or complex locations, use URL mode instead. Search mode only.

## `minPrice` (type: `integer`):

Only keep listings priced at or above this amount (applied to the numeric price). Listings with no price are kept. Search mode only: results from a pasted URL in URL mode are not price filtered.

## `maxPrice` (type: `integer`):

Only keep listings priced at or below this amount (applied to the numeric price). Listings with no price are kept. Search mode only: results from a pasted URL in URL mode are not price filtered.

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

Paste tutti.ch links: search pages (/de/q/..., /fr/q/..., /it/q/..., or /q/... without a language) and single listing pages (/de/vi/...). Pagination (?page=N) and sorting (?sorting=) in a pasted search link are honoured, apex and mobile hosts are normalised, and listing links are scraped as one listing each. Seller pages (/seller?id=...) and category landing pages (/de/cat/...) are not supported and are skipped with a warning; paste a /q/ search link instead. The search-mode fields above (keyword, category, locations, min price, max price) are ignored here, and so is Site language, because a pasted URL carries its own. The Sorting field below does still apply: it sets the ordering used from the page after the pasted one onward. Leaving this field untouched in URL mode scrapes one example of each distinct supported link shape (language-less and apex/mobile links normalise to one of them). URL mode only.

## `sortBy` (type: `string`):

Result ordering. tutti.ch defaults to newest first. Applies in BOTH search mode and URL mode. In URL mode the page you pasted is fetched exactly as given, and every following result page is then requested with this ordering, so pick the value that matches your pasted URL if you want its own sorting kept.

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

Which tutti.ch language edition to scrape (affects URL path and localized slugs). Search mode only: in URL mode the language is read from the pasted URL and from the page itself, so this field is ignored there.

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

Adds GPS coordinates, ALL image URLs, seller account ID, member-since date, phone (when published), street address and structured properties. Costs one extra request per listing.

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

How many result pages to walk per search/URL (30 listings per page). Leave empty to walk every result page (tutti.ch itself stops serving results around page 100 = 3000 listings per search). A category search in search mode with a listing cap walks at least 40 pages regardless of this value, because the category filter runs client-side.

## `maxAgeHours` (type: `integer`):

Only keep listings posted within the last N hours. 0 or empty = no age limit. With sort = Newest first the walk stops as soon as a whole page is older than the limit.

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

Hard cap on total listings across all searches. 0 means no limit (still bounded by maxPages). Maximum 100000.

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

tutti.ch is reachable from Swiss residential IPs. Residential proxy with country = CH is strongly recommended. Datacenter and free-tier connections are usually rejected by the upstream edge.

## `maxResidentialPercent` (type: `integer`):

Soft cap on how much of the run may use residential proxy. Once exceeded, the actor automatically downgrades to the cheaper rotating backup pool for remaining requests. 100 = never downgrade on this signal.

## `residentialBudgetMb` (type: `integer`):

Approximate residential bandwidth budget for the run, in megabytes. Once the estimated residential transfer exceeds this, the actor downgrades to the cheaper backup pool. 0 = no budget limit.

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

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by listing id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run.

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

Turn this on for daily or recurring monitoring of the same search. The first run returns every matching listing as NEW. Later runs return only NEW, UPDATED and REAPPEARED listings; unchanged listings are neither returned nor billed. Every returned row gets changeType, changedFields, firstSeenAt and lastSeenAt. A listing counts as UPDATED when any field other than these ignored ones changed: scraped_at, timestamp, highlighted (the posted/renewed time and the promoted flag change without any content change). Everything else counts, for example price, title, description, images, seller, location, properties, so a changed price or description returns the listing as UPDATED with the field names in changedFields. Ignored fields are still returned in each row. Add your own with the ignore list below. State is kept per search (mode, keyword, category, locations, price range, language, detail setting, URLs), never per item limit, page limit or maximum listing age. Use State key to name a campaign. To continue one interrupted run instead, use Resume from a previous run 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 derive a key from the search settings, so different searches never mix state.

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

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. These rows are delivered and billed like any other row, so leave it off unless you want the full snapshot every run.

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

Off by default. Turn on to also return listings seen in a previous run that are no longer found, marked EXPIRED. Only produced after a run that read the whole search to its last page: not when Max listings or Max pages stopped it, not for a pasted single listing or a link that starts after page 1, not with a maximum listing age, not with Resume, and not when a page or detail page failed. These rows are delivered and billed like any other row, and they count toward Max listings: any beyond it are reported on a later run.

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

Output field names that should NOT make a listing count as UPDATED, even when they changed. Always ignored: scraped_at, timestamp, highlighted (listed here so you know; you do not need to add them). A name without a dot matches that field at any nesting depth (for example text inside properties); a dotted name matches from the top of the row. 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",
  "keyword": "velo",
  "category": "all",
  "locations": [
    "Zürich"
  ],
  "urls": [
    "https://www.tutti.ch/de/q/marktplatz/Ak8CrgIWcA",
    "https://www.tutti.ch/de/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/de/q/velos-fahrraeder/Ak8CoYmljeWNsZXOUwMDAwA?page=2&sorting=cheapest",
    "https://www.tutti.ch/de/q/velos-bern/Ak8CoYmljeWNsZXOUwMDAkZOobG9jYXRpb26vZ2VvLWNhbnRvbi1iZXJuwA",
    "https://www.tutti.ch/fr/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/de/q",
    "https://www.tutti.ch/de/vi/83641798",
    "https://www.tutti.ch/de/vi/zuerich/oberstrass/sport-outdoor/velos/price-citybike/80329196",
    "https://www.tutti.ch/it/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/fr/vi/80442365",
    "https://www.tutti.ch/de/vi/specialized-hotrock/83638829"
  ],
  "sortBy": "newest",
  "language": "de",
  "fetchDetails": false,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CH"
  },
  "maxResidentialPercent": 100,
  "residentialBudgetMb": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "keyword": "velo",
    "urls": [
        "https://www.tutti.ch/de/q/marktplatz/Ak8CrgIWcA",
        "https://www.tutti.ch/de/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/de/q/velos-fahrraeder/Ak8CoYmljeWNsZXOUwMDAwA?page=2&sorting=cheapest",
        "https://www.tutti.ch/de/q/velos-bern/Ak8CoYmljeWNsZXOUwMDAkZOobG9jYXRpb26vZ2VvLWNhbnRvbi1iZXJuwA",
        "https://www.tutti.ch/fr/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/de/q",
        "https://www.tutti.ch/de/vi/83641798",
        "https://www.tutti.ch/de/vi/zuerich/oberstrass/sport-outdoor/velos/price-citybike/80329196",
        "https://www.tutti.ch/it/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/fr/vi/80442365",
        "https://www.tutti.ch/de/vi/specialized-hotrock/83638829"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "CH"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/tutti-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",
    "keyword": "velo",
    "urls": [
        "https://www.tutti.ch/de/q/marktplatz/Ak8CrgIWcA",
        "https://www.tutti.ch/de/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/de/q/velos-fahrraeder/Ak8CoYmljeWNsZXOUwMDAwA?page=2&sorting=cheapest",
        "https://www.tutti.ch/de/q/velos-bern/Ak8CoYmljeWNsZXOUwMDAkZOobG9jYXRpb26vZ2VvLWNhbnRvbi1iZXJuwA",
        "https://www.tutti.ch/fr/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/de/q",
        "https://www.tutti.ch/de/vi/83641798",
        "https://www.tutti.ch/de/vi/zuerich/oberstrass/sport-outdoor/velos/price-citybike/80329196",
        "https://www.tutti.ch/it/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
        "https://www.tutti.ch/fr/vi/80442365",
        "https://www.tutti.ch/de/vi/specialized-hotrock/83638829",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "CH",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/tutti-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",
  "keyword": "velo",
  "urls": [
    "https://www.tutti.ch/de/q/marktplatz/Ak8CrgIWcA",
    "https://www.tutti.ch/de/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/de/q/velos-fahrraeder/Ak8CoYmljeWNsZXOUwMDAwA?page=2&sorting=cheapest",
    "https://www.tutti.ch/de/q/velos-bern/Ak8CoYmljeWNsZXOUwMDAkZOobG9jYXRpb26vZ2VvLWNhbnRvbi1iZXJuwA",
    "https://www.tutti.ch/fr/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/de/q",
    "https://www.tutti.ch/de/vi/83641798",
    "https://www.tutti.ch/de/vi/zuerich/oberstrass/sport-outdoor/velos/price-citybike/80329196",
    "https://www.tutti.ch/it/q/velos/Ak8CoYmljeWNsZXOUwMDAwA",
    "https://www.tutti.ch/fr/vi/80442365",
    "https://www.tutti.ch/de/vi/specialized-hotrock/83638829"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CH"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/tutti-ch-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/tutti-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/33DggHseDjgS2hKua/builds/9OtEB3xbH9az5frZT/openapi.json
