# Tori.fi Scraper: Finland Classifieds and Prices (`automation_craft/tori-scraper`) Actor

Tori.fi ads from Finland's largest second hand marketplace by keyword, category, region, postal code, radius or shop, 15,000 ads a search, no login. Rows: price in EUR, sale or give away, private or company, ToriDiili; ad pages add condition, Y-tunnus. Free price summary, JSON or CSV, pay per ad.

- **URL**: https://apify.com/automation_craft/tori-scraper.md
- **Developed by:** [Automation Craft](https://apify.com/automation_craft) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.85 / 1,000 listings

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

### Tori.fi Scraper: Finland Classifieds and Prices

**Tori scraper** for Tori.fi, Finland's largest second hand marketplace: search Tori's marketplace (furniture, home goods,
electronics, phones, clothes, children's things, sports and outdoor gear, car parts and tyres, tools, collectibles and every other
category) by keyword, category, region, municipality, district or Finnish postal code, map radius, Tori's own filters, a shop, a
pasted Tori link or an ad id, and get every ad as one row: title, price in euros, ad type (for sale, give away, wanted), place with its
region, municipality and district, map point, photos, private seller or company, the shop's name, ToriDiili, buy now and free
delivery flags, brand and attributes. Every search also gives a free price summary of the ads it met, by region and by category. Add
the ad page for the full description, every attribute (condition, size, material), the category path, the postal code, the edit time,
and a shop's profile with its phone, address, website and Finnish business id (Y-tunnus). No login, no cookies, no residential proxy.

Who uses it: Finnish resellers and flippers checking second hand prices and hunting deals, people who want wider or faster lists than
Tori's own saved searches, small shops watching what other shops list on Tori, second hand price analysts by region and category,
and teams feeding Tori data into their own sheets, apps or AI agents.

### Quick start

1. Open the **Input** tab. Add **Keywords** (`sohva`, `lastenrattaat`, `iittala`, `talvirenkaat`) or **Categories** by Tori's Finnish
   name (`Koti ja sisustus`, `Sohvat ja lepotuolit`, `Puhelimet ja tarvikkeet`), and **Places** (`Uusimaa`, `Tampere`, `Malmi`, or a
   postal code such as `00700`).
2. Narrow it with Tori's filters: price, seller (`Yksityinen` or `Yritys`), **Condition** (`Uusi`, `Kuin uusi`, `Hyvä` ...), **Ad
   type** (`Myydään`, `Annetaan`, `Ostetaan`), **Brands**, **ToriDiili only**, **Published today**. Or give **Shops (all ads of a
   shop)**, or paste Tori links into **Search links** and **Ad links or Tori ad ids**.
3. Turn on **Include the ad page (Ad details)** for the full ad if you want it. Set **Maximum rows** and click **Start**. Download the
   rows as JSON, CSV or Excel, or read them through the Apify API.

The prefilled input reads 20 sofas (`sohva`) in Uusimaa: 20 Listing events and 1 Results page, $0.0250 at Gold and $0.0320 on the Free
plan with the start fee (about 10 seconds on a test run of 2026-10-25). For a daily feed, schedule the run in Apify (set **Posted
within (hours)** to 24 and turn on **Published today**) and keep the ad ids you have seen in your own sheet or flow: this Actor keeps
no memory between runs.

### What you get

One row per ad (`type: "listing"`, `vertical: "marketplace"`), one free coverage row per search with its price summary
(`type: "search"`), free status rows for anything that could not be delivered (`type: "status"`) and a run summary
(`type: "summary"`). Every price is in euros (`currency: "EUR"`). A field Tori does not show for an ad is `null`.

#### Fill rates measured

The share of delivered rows that carry each field, measured on the runs of 2026-10-25 on the test actor (builds 0.1.1 and 0.1.2) and
on this Actor (build 0.1.1); run files `platform-results/<run>.items.json`, each ad counted once per column, the runs with **Include
personal data** on left out. Search row: 20,089 rows from 40 runs (`tori-r1-sweep-category` 16,185, `tori-t2-worst-1000` 997,
`tori-r1-worst-1000` 994, `tori-t2-300-rows` 600, `tori-t2-keyword-match-off` 502, 35 smaller runs 811). Ad details: 868 rows from 18
runs (`tori-r1-worst-details-300` 300, `tori-t2-worst-details-300` 297, `tori-r1-stop-at-timeout` 80,
`tori-r1-worst-50-searches-one-row-details` 44, 14 smaller runs 147). 1,712 of the search rows and 83 of the Ad details rows are a
company's ads, so the company fields stand for about one row in ten. `contacts` is null on every ad with **Include personal data**
off, so it is empty here; Tori gave no sold time and no first publication time on these runs. "n/a": filled only with the ad page.

| Field | Search row | Ad details |
|---|---|---|
| `price` | 100% | 100% |
| `tradeType` | 100% | 100% |
| `region` | 100% | 100% |
| `municipality` | 100% | 100% |
| `district` | 100% | 100% |
| `location` | 100% | 100% |
| `latitude` | over 99% | 99% |
| `imageUrl` | over 99% | over 99% |
| `publishedAt` | 100% | 98% |
| `sellerType` | 100% | 100% |
| `companyName` | 8% | 10% |
| `brand` | 6% | 34% |
| `searchAttributes` | 8% | 34% |
| `labels` | 100% | 98% |
| `description` | n/a | 100% |
| `category` | n/a | 100% |
| `condition` | n/a | 86% |
| `attributesById` | n/a | 91% |
| `postalCode` | n/a | 100% |
| `editedAt` | n/a | 100% |
| `companyId` | n/a | 10% |
| `companyPageUrl` | n/a | 10% |
| `companyOrgNumber` | n/a | 9% |
| `companyPhone` | n/a | 10% |
| `companyWebsite` | n/a | 7% |
| `streetAddress` | n/a | 10% |
| `companyEmail` | n/a | 0% |
| `contacts` | 0% | 0% |
| `soldAt` | 0% | 0% |
| `firstPublishedAt` | 0% | 0% |

#### On every row

| Field | What it is |
|---|---|
| `adId`, `url`, `title` | Tori's ad id (Ilmoituksen tunnus), the ad's link, its title as served |
| `price`, `currency` | the price as a number, in EUR, as Tori serves it: Tori gives `0` on give away and wanted ads and on an ad sold without a price (550 of 552 give away rows and all 47 wanted rows of the test runs) |
| `tradeType`, `tradeTypeEnglish` | "Myydään" (for sale), "Annetaan" (given away free) or "Ostetaan" (wanted), and in English |
| `status`, `soldAt` | `active`, or `sold` when Tori marks the ad sold, with the time when Tori gives one |
| `location`, `municipality`, `district`, `region`, `regionSource` | Tori's place ("Malmi, Helsinki"), its municipality and district, and the ad's region (maakunta: one of Tori's 19, `Uusimaa`, `Pirkanmaa`, `Varsinais-Suomi` ...) read from Tori's own location tree by the municipality (`toriMunicipality`) |
| `latitude`, `longitude`, `coordinateAccuracy` | the ad's map point |
| `publishedAt`, `editedAt` | when Tori published the ad, when it was last edited (ad page) |
| `sellerType`, `companyName`, `companyId`, `companyPageUrl` | `private` or `business`, the shop's name, its Tori id (ad page) and its shop page on Tori (ad page) |
| `brand`, `searchAttributes` | the brand Tori shows on the search row, the search row's own attributes (shoe and clothing size, memory, console type) |
| `shippingAvailable`, `buyNow`, `sellerPaysShipping` | Tori's flags: sold with Tori's own shipping and payment service (ToriDiili), "Osta heti" (buy now), "Ilmainen toimitus" (free delivery) |
| `labels` | Tori's own labels on the search row ("Yksityinen", "Yritys", "ToriDiili", "Osta heti", "Ilmainen toimitus") |
| `imageUrl`, `imageUrls`, `imageCount` | the main image, every image link, their count |
| `searchKey`, `searchLabel`, `position`, `source`, `detailsIncluded`, `scrapedAt` | the search the row came from, its place in the results, `search` or `link`, whether the ad page was read, the read time |
| `paidPlacement` | with **Mark paid placements** on: `true` when the row is the paid ad Tori shows above the search |

#### With the ad page (Ad details)

| Field | What it is |
|---|---|
| `description`, `descriptionText` | the seller's text as served and as plain text |
| `mainCategory`, `subCategory`, `category`, `categoryPath` | Tori's category path |
| `condition`, `conditionEnglish` | Tori's condition label ("Hyvä"), and in English ("good") |
| `attributes`, `attributesById` | the ad's attributes with Tori's Finnish labels, and the same values under Tori's own attribute ids (`condition`, `shoe_size`, `width` ...) |
| `postalCode`, `postalName`, `streetAddress` | the ad's postal code and place, and a shop's street |
| `companyOrgNumber`, `companyPhone`, `companyWebsite`, `companyAddress`, `companyLogo` | a shop's profile: Finnish business id (Y-tunnus, check digit verified), phone, website, address, logo |
| `isWebshop`, `mapImageUrl` | Tori's webshop flag and Tori's map image of the ad's point |

#### Example row

One shop ad of a run of this Actor on 2026-10-25 (`tori-r1-shop-ids`, build 0.1.1) with the ad page, trimmed to the main fields
(the full row has 69 keys; `descriptionText` begins "Myydään erinomaisessa kunnossa oleva LG 70" 4K UHD Smart TV ..."):

```json
{
  "type": "listing", "vertical": "marketplace", "kind": "all",
  "adId": "48437058", "url": "https://www.tori.fi/recommerce/forsale/item/48437058",
  "title": "Takuu! LG 70\" 4K UHD Smart TV, vaihto, kuljetus", "price": 329, "currency": "EUR",
  "tradeType": "Myydään", "tradeTypeEnglish": "for sale", "status": "active",
  "condition": "Kuin uusi", "conditionEnglish": "like new",
  "mainCategory": "Elektroniikka ja kodinkoneet", "subCategory": "Ääni ja kuva", "category": "TV",
  "location": "Siltamäki, Helsinki", "municipality": "Helsinki", "district": "Siltamäki", "region": "Uusimaa", "regionSource": "toriMunicipality",
  "postalCode": "00740", "postalName": "Helsinki, Siltamäki, Uusimaa", "latitude": 60.268816, "longitude": 24.985892,
  "publishedAt": "2026-10-24T22:10:11.000Z", "editedAt": "2026-10-24T15:18:30.192342+02:00",
  "sellerType": "business", "companyName": "Koudria TV-kauppa", "companyId": "1709285", "companyOrgNumber": "3564523-9",
  "companyPageUrl": "https://www.tori.fi/tori-kaupat/koudria",
  "shippingAvailable": false, "buyNow": false, "sellerPaysShipping": false, "labels": ["Yritys"], "imageCount": 6,
  "attributesById": {"condition": "Kuin uusi", "tv_screen_size": "70 in", "sound_image_brand": "LG"},
  "searchLabel": "in the marketplace of the shop 1709285", "position": 2, "source": "search", "detailsIncluded": true
}
```

#### The coverage row of each search, with its price summary (free)

Every search ends with one free row: Tori's count of ads (`adsOnTori`), the filters sent (`filtersSent`), Tori's order (`order`),
pages and rows read, distinct ads met (`distinctAdsMet`), ads delivered (`adsDelivered`), ads already delivered by an earlier search
of the run (`alreadyDelivered`), ads left out by a check (`filteredOut`, `filteredOutBy`), slices read past Tori's reach
(`slicesRead`), ads that stayed beyond reach (`adsBeyondReach`), whether the search was read to its end (`complete`) and why it
stopped (`stoppedBy`). A shop's search names the shop (`shopId`). With **Mark paid placements** on, `paidPlacement` names the paid ad
Tori showed above the search and whether it stood among the results.

Its `priceSummary` gives the count, lowest, quartiles (`p25`, `median`, `p75`), mean and highest asking price of the ads the search
met that passed every check on the row, the count of ads without a price (`withoutPrice`), and the same per Tori region (`byRegion`)
and per category (`byCategory`), each part with its count, median, lowest, highest and mean, largest first. Each category you name is
its own search, so asking for several categories gives you prices by category and by region in one run. Turn it off with **Price
summary per search**; it is free either way. Tori's price of `0` (give away and wanted ads) counts as a price, so set **Ad type** to
`Myydään` to leave give away and wanted ads out of the summary.

#### Add-on: Ad details (the ad page)

**Include the ad page (Ad details)** reads each ad's page and merges it into the row (the fields above). It is billed as one Ad details
per row that carries its ad page and takes about a second per row. The ad page is always read (and billed) on an ad link.

#### A shop's whole stock

**Shops (all ads of a shop)** takes a shop's Tori id (the `companyId` of a company row with its ad page) or its shop page link
(`https://www.tori.fi/tori-kaupat/<shop>`, the `companyPageUrl` of a company row): the shop's id is read from its page once (free),
then every ad of that shop is searched with Tori's own shop filter, narrowed by your keywords, categories and filters. A shop page Tori
does not serve is a free status row.

### How much does it cost to scrape Tori?

You pay per event, with Apify's store discounts by plan. The ad page is billed only when you turn it on (and on ad links) and only
on a row that carries it.

| Event | Free plan | Bronze | Silver | Gold |
|---|---|---|---|---|
| Listing (one ad row, primary) | $1.19 per 1,000 | $1.09 per 1,000 | $0.99 per 1,000 | $0.85 per 1,000 |
| Ad details (one row that carries its ad page) | $0.84 per 1,000 | $0.77 per 1,000 | $0.70 per 1,000 | $0.60 per 1,000 |
| Results page (a search page that brings an ad new in the run and delivers one; up to 300 ads) | $0.70 per 1,000 pages | $0.64 per 1,000 pages | $0.58 per 1,000 pages | $0.50 per 1,000 pages |
| Actor start (once per run at the default 256 MB) | $0.0075 | $0.0075 | $0.0075 | $0.0075 |

Platinum and Diamond plans pay the Gold price. The Actor start is a flat fee per run, with no plan discount.

The Store pricing card shows these same prices per 1,000 events: "$0.85 / 1,000" on the Listing row means one ad costs $0.00085 at
Gold, and "$0.84 / 1,000" on the Ad details row means one ad page costs $0.00084 on the Free plan.

- **Listing** is charged once per ad delivered as a row, from a search, a shop or a link. An ad met twice in one run (two keywords,
  two slices) is delivered and billed once.
- **Results page** is charged once per Tori search page the run reads that brings at least one ad not met before in the run and from
  which an ad is delivered (up to 300 ads a page, so about $0.002 per 1,000 ads at Gold). It pays for the searches that deliver only a
  few rows (many keywords, many shops), so the row price stays low. A page of repeats, a page whose ads are all left out by a filter, a
  page Tori did not apply, the first page of a split, an empty search, ad links and every page of a run's restarted attempt are free.
- **Ad details** is charged only with **Include the ad page (Ad details)** on, or on an ad link, and only on a row that carries its ad
  page.
- Never billed: repeats in the run, ads left out by a filter (the keyword tail below included), a removed or unknown ad link, a shop page
  Tori does not serve, a refused setting, a search that finds nothing, coverage rows, status rows and the summary.
- **Every run is charged the Actor start fee ($0.0075), also when it finds nothing** (an empty input, a filter no ad passes, a refused
  setting): such a run costs $0.0075.

Worked examples, computed from the prices above with the start fee included. The rows marked "measured" billed exactly these events
on a test run of 2026-10-25; a real run can bill one or two more Results pages where a page brings only a few new ads.

| Run | Events | Free plan | Bronze | Silver | Gold |
|---|---|---|---|---|---|
| The prefill: 20 sofas in Uusimaa (measured) | 20 Listing, 1 Results page | $0.0320 | $0.0299 | $0.0279 | $0.0250 |
| 1,000 ads of a category, search rows only (measured) | 1,000 Listing, 4 Results pages | $1.2003 | $1.1001 | $0.9998 | $0.8595 |
| 300 ads with their ad pages (measured) | 300 Listing, 300 Ad details, 1 Results page | $0.6172 | $0.5661 | $0.5151 | $0.4430 |
| 1,000 ads with their ad pages | 1,000 Listing, 1,000 Ad details, 4 Results pages | $2.0403 | $1.8701 | $1.6998 | $1.4595 |
| 50 keywords, one ad each (measured) | 50 Listing, 50 Results pages | $0.1020 | $0.0940 | $0.0860 | $0.0750 |
| A run that delivers nothing | none | $0.0075 | $0.0075 | $0.0075 | $0.0075 |

Set the run's maximum cost in Apify high enough for large runs: a row whose events do not all fit under it is withheld whole, never
delivered unbilled or billed without its data.

### Input

The Actor has 52 settings; every one is in the **Input** tab with its own help text. The main ones:

| Setting | What it does |
|---|---|
| `keywords` | Search words, each its own search (`sohva`, `iittala`, `lego technic`). Tori matches them in the ad texts. |
| `onlyKeywordMatches` | On by default: a keyword search delivers only ads whose title (or description, with the ad page) carries every word of the search; the rest of Tori's results are left out free (below). |
| `categories` | Categories, sub categories or product categories by Tori's Finnish name (`Koti ja sisustus`, `Sohvat ja lepotuolit`, `Puhelimet ja tarvikkeet`) or value, each its own search. |
| `shopIds` | Shops by id or shop page link, each a search of every ad of that shop. |
| `searchUrls` | Tori search links (`https://www.tori.fi/recommerce/forsale/search?...`), read with all their filters. A link keeps its own filters: Tori's filters set here (places, price, seller, condition, ad type, brands) apply only to the searches built from keywords, categories and shops, so a run of links with such a filter and nothing else to search is refused with a free row (the start fee is charged). Put the filter into the link on Tori, or clear the prefilled **Places**. |
| `adUrls` | Ad links (`/recommerce/forsale/item/<id>`, `https://www.tori.fi/<id>`) or ad ids, each read with its ad page. A removed or unknown ad is a free status row; Tori's old ad addresses (before 2024) no longer lead anywhere and are refused, free. A list that starts with about 30 removed or unknown ads in a row stops there on the unpaid allowance (the rest is listed as not reached, free), so put the links you trust first. |
| `locations` | Tori's 19 regions (`Uusimaa`, `Pirkanmaa`, `Varsinais-Suomi`), its 301 municipalities (`Helsinki`, `Tampere`, `Oulu`) or 2,926 districts (`Malmi`, `Hervanta`) by Tori's name or value, or a Finnish postal code (`00700` is Malmi in Helsinki, `33720` Hervanta in Tampere: Tori files every district under its postal code). Several places are one search. A postal code Tori lists no district for (a post box code) is a free row, and the other places run. |
| `latitude`, `longitude`, `radiusKm` | Tori's own map radius (1 to 500 km) around a point in Finland. |
| `minPrice`, `maxPrice`, `sellerType`, `postedToday` | Price in euros, private sellers or companies only (Tori's "Yksityinen" and "Yritys"), Tori's "Julkaistu tänään" (published today). |
| `conditions`, `tradeTypes`, `brands`, `shippingOnly` | Tori's condition labels, ad type ("Myydään", "Annetaan", "Ostetaan"), brand (`IKEA`, `Iittala`, `Marimekko`), ToriDiili only. |
| `attributeFilters` | Any other filter of the search's own Tori filter list, by name or Finnish label: `{"shipping_types": ["Ilmainen toimitus"]}` for free delivery. |
| `postedAfter`, `postedWithinHours`, `postedBefore`, `includeKeywords`, `requireAllKeywords`, `excludeKeywords`, `titleOnly`, `excludeAdIds` | Filters checked on every row; an ad left out is free. Days are Finnish days. **Posted before** reads the newer ads in front and leaves them out free, so a day far back can stop the run on its allowance for unpaid reads. |
| `maxItems`, `maxItemsPerSearch`, `maxPagesPerSearch`, `maxRowsPerSearch`, `startPage` | Paid rows per run (default 100), per search, pages per search, rows read per search; **Start page** reads every search from that page of Tori's own results (53 ads a page) to go on where an earlier run stopped (such a search is not split and its coverage row says it is not complete). |
| `includeDetails`, `includePersonalData`, `markPaidPlacements` | The ad page (Ad details); the personal data switch (below); the paid ad flag (one free extra read per search). |
| `priceSummary`, `includeRawData`, `descriptionFormat`, `descriptionMaxLength`, `outputFields`, `compactOutput`, `excludeEmptyFields`, `extractContacts`, `phoneExtractionMode` | The price summary per search (on by default); Tori's search row; description `asServed`, `text` or `markdown` and a length cut; your own field list or the most used fields only; empty fields left out; e-mails, phones, websites and social profiles written in the texts (companies' ads always, private ads with personal data on). |
| `failIfIncomplete`, `completenessTolerancePercent`, `completenessToleranceAbsolute`, `maxFailedRequestsPercent`, `proxyConfiguration`, `requestsPerSecond`, `maxConcurrency`, `maxRequestRetries` | Fail the run when a search is incomplete or too many reads failed; your own bar for a complete search; your own proxy links (Apify's datacenter proxy by default, which every Tori door answers); the pace (0.2 to 2 requests a second, default 1), requests in flight (1 to 4), retries (0 to 5). |

A run reads at most 300 searches. A monitor or alert setting refuses the run with a free row that says this Actor keeps no monitor,
and an empty input is refused with a free row (only the run's start fee is charged).

#### Example inputs

Sofas and armchairs in Uusimaa and Pirkanmaa, with their ad pages:

```json
{ "categories": ["Sohvat ja lepotuolit"], "locations": ["Uusimaa", "Pirkanmaa"], "includeDetails": true, "maxItems": 200 }
```

Strollers in good condition sold with ToriDiili, private sellers, at most 300 euros:

```json
{ "keywords": ["lastenrattaat"], "conditions": ["Hyvä"], "shippingOnly": true, "sellerType": "private", "maxPrice": 300, "maxItems": 100 }
```

Everything given away free in Helsinki today:

```json
{ "tradeTypes": ["Annetaan"], "locations": ["Helsinki"], "postedToday": true, "maxItems": 200 }
```

Every ad of one shop, with the shop's profile:

```json
{ "shopIds": ["https://www.tori.fi/tori-kaupat/koudria"], "includeDetails": true, "maxItems": 500 }
```

#### Tori's keyword search and its tail

Tori ranks a keyword's results by relevance and ends them with ads that do not carry the word (related goods first, then unrelated
ones). With **Only ads that carry the search words** on (the default), a row of a keyword search is delivered only when its title (or
its description, with the ad page read) holds every word of the search; the others are left out, free, and counted on the coverage
row (`filteredOutBy.onlyKeywordMatches`). Words match as parts of words ("sohva" matches "kulmasohva"), and "a OR b" means either. An
ad whose title lacks the word but whose description has it (a tyre ad with "Kausi: Talvirenkaat" in its text) is delivered only with
the ad page read, or with the check turned off. Measured on a test run of 2026-10-25: "kajakki" met all 745 ads Tori counted and
delivered 243, 502 left out free. Turn it off to get every ad Tori's search returns.

#### Every filter is checked

Every named filter is resolved against the search's own Tori filter list. A value the list does not offer (a place, a category, a
condition) is a free row that lists the values it does offer, and the search delivers nothing: an unknown value never widens a search.
Every page Tori answers must echo back the filters sent, so a page where Tori dropped one becomes a free row and nothing from it is
delivered or billed.

#### Past Tori's 15,000 ad reach

Tori serves at most 50 pages of one search, up to 300 ads a page: 15,000 ads. Past page 50 it answers an error; the walk stops at the
lower of page 50 and Tori's own last page. Tori counted about 3.48 million ads on 2026-10-25. When Tori counts more ads than one search
can reach and you want more, the search is split into slices by Tori's own counts: by category, sub category and product category,
then by place, then by price bands. Each slice is read in Tori's own order, an ad met in an earlier slice is skipped free, and the
coverage row says how many slices were read and how many ads stayed beyond reach (`adsBeyondReach`). A search is `complete` when its
slices met about every ad Tori counted (at least 98 percent less 2 ads on a split search, 99 percent less 2 on one search, or your own
bar set with `completenessTolerancePercent` and `completenessToleranceAbsolute`). Measured on this Actor on 2026-10-25 (`tori-r1-sweep-category`, build 0.1.1): the category Soittimet, which Tori counted at 16,540
ads, was read in 2 slices and 56 pages in 5 minutes 10 seconds and delivered 16,186 distinct ads; the coverage row named the 354
ads Tori counts on the category but on none of its parts, so no slice can reach them, and the search was marked not `complete`
(97.9 percent met, under the 98 percent bar). The run cost 16,186 Listing events and 55 Results pages.

#### Restarts

If Apify restarts a run on another server, the run reads its own dataset back: the rows already delivered are neither delivered nor
charged again, and the pages of the restarted attempt are free. Nothing is billed twice.

#### Personal data

A private seller is an ad Tori marks as private ("Yksityinen"). With **Include personal data** off (the default), these fields are
null on a private seller's row: the seller's Tori user id (`sellerId`), street (`streetAddress`), the contacts found in the texts
(**Extract contacts from texts**) and the raw search row; a private seller's map point is rounded to 2 decimals (about 1 km) when Tori
gives it to the address. On every ad, a contact person's name, phone and e-mail (`contacts`) are null with the switch off. Turning the
switch on adds all of these. A shop's name, id, business id (Y-tunnus), address, phone and website are business data and always
delivered. Titles and descriptions are delivered exactly as Tori serves them, so an ad's own text can name a person or give a phone
number. Tori shows no private seller's name to visitors who are not logged in, so this Actor never has it. You are responsible for a
lawful use of personal data, GDPR included.

### FAQ

#### Does Tori.fi have an API?

This Actor works as one: it reads Tori's public search and ad pages without logging in and gives you the rows through the Apify API as JSON, CSV or Excel. Each search also ends with a free coverage row that carries Tori's own count of ads (`adsOnTori`).

#### Is it legal to scrape Tori.fi?

This Actor reads only the public search and ad pages any visitor sees without logging in, never sends a sort parameter or opens the profile, contact and map pages Tori's robots.txt disallows, and withholds a private seller's personal data unless you turn Include personal data on. You are responsible for a lawful use of the data, the GDPR included: check Tori's terms and Finnish law for your own purpose.

#### How do I get notified of new ads on Tori.fi?

This Actor sends no notifications and keeps no memory between runs. Schedule the run in Apify with Published today on (and Posted within (hours) for a shorter window): each run delivers the ads Tori published in that window, and you keep the ad ids you have already seen in your own sheet or flow.

#### How much are things worth on Tori.fi?

Each row carries the ad's asking price in euros as Tori shows it. Each search's free coverage row gives the count, lowest, quartiles, median, mean and highest asking price of the ads it met, and the count, median, lowest, mean and highest per Tori region and per category.

#### What is ToriDiili, and can I get only ToriDiili ads?

ToriDiili is Tori's own shipping and payment service. Turn on ToriDiili only to read only those ads; every row carries the ToriDiili flag (`shippingAvailable`), the buy now flag (`buyNow`) and the free delivery flag (`sellerPaysShipping`).

#### Why does this Actor run with limited permissions?

It needs nothing outside its own run: it reads Tori and writes only to its own run's dataset and key-value store, and it opens no named storage. Limited permissions keep the rest of your Apify account out of its reach.

### What this Actor does NOT do

- **No other sites.** This listing reads Tori only; one run never mixes sites, so every row is a Tori row. The family's other
  listings read their own sites (below).
- **No cars, homes or jobs.** Tori's own car site closed in 2024 and its housing ads moved to another site in 2023; Tori has no job
  search. This Actor reads Tori's marketplace, where car parts, tyres and accessories are a category of their own.
- **No sort order.** Tori's robots.txt asks robots not to send its sort parameter, so every search is read in Tori's own default order
  (newest first without words; relevance with words); a pasted link's sort is dropped with a free note.
- **No monitor and no alerts.** It keeps no memory between runs: no "new only" rows, no price change, sold or removal tracking across
  runs, no Telegram, Slack, Discord, WhatsApp or webhook alerts; a run that asks for one is refused with a free row (only the run's start
  fee is charged, as on every run) and reads nothing. Schedule the run in Apify (**Posted within (hours)** set to 24 with **Published
  today** on) and compare the rows in your own sheet or flow by `adId`. Within one run, a removed ad link is a free status row.
- **No private seller name, no messages.** Tori's ad pages name no private seller to visitors who are not logged in; this Actor never
  logs in and never contacts a seller. A shop's name, phone and address are on the ad page and delivered.
- **No login only data.** Saved searches, favourites, messages, a seller's profile pages (`/profile`, disallowed by Tori's robots.txt)
  and anything behind a Tori account are not read.
- **No sale prices.** Prices and price summaries are asking prices of ads on Tori.
- **No view counts.** Tori's ad pages carry no view count.
- **No paid ad rows outside the results.** The paid ad Tori shows above a search is never delivered as a row of its own; with **Mark
  paid placements** on, a delivered row that is that paid ad says `paidPlacement: true`.
- **No photo downloads.** Photo links only.
- **No guessed spellings and no empty input sample.** A value is used as you send it (after trimming and case) or refused with a free
  row listing Tori's own values; an input with nothing to search is refused with a free row, and the Console prefill is the example.
- **No unpaid crawling.** A run stops with a status row when its unpaid work (pages whose ads were all filtered out or already
  delivered, removed ads, the reads that check a search's filters, shop pages) passes a small allowance (USD 0.0004 plus a share of
  every billed event; about 30 removed ad links in a row before any delivered ad): narrow the search or loosen the filters checked on
  each row.

### API and integrations

Run it from the Apify API, the Apify client for JavaScript or Python, Make, Zapier, n8n or any HTTP client. Example with the API (put
your own token in place of `<YOUR_TOKEN>`):

```bash
curl -X POST "https://api.apify.com/v2/acts/automation_craft~tori-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["sohva"], "locations": ["Uusimaa"], "maxItems": 20}'
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_TOKEN>' });
const run = await client.actor('automation_craft/tori-scraper').call({ categories: ['Puhelimet ja tarvikkeet'], locations: ['Pirkanmaa'], maxItems: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_TOKEN>")
run = client.actor("automation_craft/tori-scraper").call(run_input={"keywords": ["iittala"], "includeDetails": True, "maxItems": 100})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Every row carries `type`; keep `type == "listing"` for the ads, and read the coverage rows (`type == "search"`) for the price
summaries and to see whether each search was read in full. The run report (`REPORT` in the run's key-value store) sums up the run.

#### Use it from an AI agent (MCP)

AI agents can run this Actor as a tool through Apify's MCP server: connect your MCP client to `https://mcp.apify.com` and add
`automation_craft/tori-scraper` to its tools. The agent sends the input as JSON (for example `{"keywords": ["polkupyörä"],
"locations": ["Turku"], "maxItems": 20}`) and reads the run's dataset back: the ad rows (`type: "listing"`), the coverage row of each
search with its price summary (`type: "search"`), status rows and the summary. For agents, set `compactOutput` (the most used fields
only, `region` included) or `outputFields`, and `excludeEmptyFields`, keep `maxItems` small, and read the coverage row to see whether a
search was read to its end and how many ads Tori counts.

### Changelog

See the Changelog tab (0.1, the launch).

### More data tools by Automation Craft

- [DBA Scraper: Denmark Classifieds, Cars, Prices](https://apify.com/automation_craft/dba-scraper)
- [Blocket Scraper: Ads, Prices, Cars and Counties](https://apify.com/automation_craft/blocket-scraper)
- [FINN.no Scraper: Torget, Cars, Homes and Jobs](https://apify.com/automation_craft/finn-scraper)
- [FINN Motor Scraper: Norway Cars, MC and Boats](https://apify.com/automation_craft/finn-motor-scraper)
- [Marktplaats Scraper: Ads, Cars, Sellers & Alerts](https://apify.com/automation_craft/marktplaats-scraper)
- [willhaben Scraper: Immobilien, Cars, Jobs & Ads](https://apify.com/automation_craft/willhaben-scraper)
- [Kleinanzeigen Scraper & API: Ads, Prices, Places](https://apify.com/automation_craft/kleinanzeigen-scraper)
- [Gumtree Scraper: UK, Australia, South Africa Ads](https://apify.com/automation_craft/gumtree-scraper)

# Changelog

This Actor's version history is a separate document: https://apify.com/automation_craft/tori-scraper/changelog.md

# Actor input Schema

## `keywords` (type: `array`):

Search words, each one its own search ("sohva", "iittala", "lastenrattaat", "talvirenkaat", "lego technic"). Tori matches them in the ad texts. Left empty, each search reads without words, newest first.

## `categories` (type: `array`):

Tori categories, sub categories or product categories by Tori's Finnish name ("Koti ja sisustus", "Sohvat ja lepotuolit", "Puhelimet ja tarvikkeet") or Tori's value ("0.78"). Each category is its own search, with its own price summary.

## `shopIds` (type: `array`):

Every ad of one Tori shop (a company seller): the shop's id (the companyId of a company row) or its shop page link (https://www.tori.fi/tori-kaupat/<shop>, the companyPageUrl of a company row; the shop's id is read from that page first, free). Each shop is its own search with Tori's own shop filter; keywords, categories and filters narrow it.

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

Tori's regions ("Uusimaa", "Pirkanmaa", "Varsinais-Suomi"), municipalities ("Helsinki", "Tampere", "Oulu") or districts ("Malmi", "Hervanta") by Tori's name, or Tori's value ("0.100018"), or a Finnish postal code ("00700", "33720"; Tori's district of that code). Several places are one search (any of them).

## `latitude` (type: `number`):

With longitude and radiusKm: search around a point in Finland (Tori's own map radius).

## `longitude` (type: `number`):

With latitude and radiusKm.

## `radiusKm` (type: `integer`):

With latitude and longitude: the distance in kilometres.

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

The lowest price in euros, sent to Tori.

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

The highest price in euros, sent to Tori.

## `sellerType` (type: `string`):

Private sellers only, or companies only (Tori's own seller filter, "Yksityinen" or "Yritys").

## `postedToday` (type: `boolean`):

Only ads Tori marks as published today ("Julkaistu tänään").

## `conditions` (type: `array`):

Tori's condition labels: "Uusi" (new), "Kuin uusi" (like new), "Hyvä" (good), "Kohtalainen" (fair), "Vaatii korjausta" (needs repair).

## `tradeTypes` (type: `array`):

"Myydään" (for sale), "Annetaan" (given away free) or "Ostetaan" (wanted).

## `brands` (type: `array`):

Brands as Tori lists them for the search ("IKEA", "Iittala", "Marimekko", "Apple", "Stokke" and about 1,700 more).

## `shippingOnly` (type: `boolean`):

Only ads sold with Tori's own shipping and payment service ("ToriDiili"). For free delivery only, use attributeFilters {"shipping_types": \["Ilmainen toimitus"]}.

## `attributeFilters` (type: `object`):

Any other filter of the search's own Tori filter list, by Tori's name or Finnish label, with values by label or value, or {"min": n, "max": n} for a range. For example {"shipping_types": \["Ilmainen toimitus"]} (free delivery). A filter the search does not offer is refused with the filters it does offer.

## `postedAfter` (type: `string`):

Only ads published on or after this day in Finland (YYYY-MM-DD), checked on every row.

## `postedWithinHours` (type: `integer`):

Only ads published within this many hours of the run's start, checked on every row. Tori lists a search without words newest first, so for a short window turn on "Published today" too (Tori then sends only today's ads).

## `postedBefore` (type: `string`):

Only ads published before this day in Finland (YYYY-MM-DD), checked on every row. Tori lists newest first, so the newer ads in front are read and left out free; a day far back can stop the run on its allowance for unpaid reads.

## `includeKeywords` (type: `array`):

Words an ad must contain (its title, and its description when the ad page is read). Any one of them, or all with requireAllKeywords.

## `requireAllKeywords` (type: `boolean`):

With includeKeywords: every word must be in the ad.

## `excludeKeywords` (type: `array`):

Ads holding any of these words are left out, free.

## `titleOnly` (type: `boolean`):

includeKeywords and excludeKeywords look at the title only.

## `onlyKeywordMatches` (type: `boolean`):

Searches made from keywords (and pasted search links with words): deliver only the ads whose title carries every word of the search, or, when the ad page is read, its title or description. Tori's own search ends a keyword's results with ads that carry none of the words (on the last pages of a search most titles lack them); those are left out, free, and counted on the coverage row. Words are matched as parts of words ("sohva" matches "kulmasohva"); "a OR b" means either. Off: every ad Tori's search returns.

## `excludeAdIds` (type: `array`):

Tori ad ids (Ilmoituksen tunnus) or ad links never to deliver.

## `searchUrls` (type: `array`):

Search pages copied from Tori (https://www.tori.fi/recommerce/forsale/search?...), read with all their filters. Tori's sort order in a link is not sent (Tori asks robots not to); the link is read in Tori's default order.

## `adUrls` (type: `array`):

Tori ad links (https://www.tori.fi/recommerce/forsale/item/<id> or https://www.tori.fi/<id>) or Tori ad ids (Ilmoituksen tunnus): each read with its ad page and billed as one Listing plus one Ad details. A removed or unknown ad is a free row.

## `maxItems` (type: `integer`):

The most paid rows (ads) the run delivers, across all searches and links.

## `maxItemsPerSearch` (type: `integer`):

The most paid rows from one search or search link.

## `maxPagesPerSearch` (type: `integer`):

The most result pages read for one search.

## `maxRowsPerSearch` (type: `integer`):

How many rows of each search are read at most, the rows left out by a filter included (no limit by default).

## `startPage` (type: `integer`):

Read every search from this page of Tori's own results (53 ads a page) instead of the first: to go on from where an earlier run stopped. Such a search is not split past Tori's 50 page reach and its coverage row says it is not complete. Given, even as 1, every search is read in Tori's own pages from that page, so runs of startPage 1, 2, 3 never overlap.

## `includeDetails` (type: `boolean`):

Read each ad's page: the full description, every attribute (condition, size, material, brand) and all images, the category path, the postal code and street (a company's), the edited time, and a shop's profile (name, address, phone, website, business id Y-tunnus, its shop page). Billed as one Ad details per row that carries its ad page; about a second per row.

## `includePersonalData` (type: `boolean`):

Deliver the fields that name or reach a private person: a private seller's Tori user id, the street and exact map point of a private seller's ad (off: the point is rounded to 2 decimals, about 1 km, when Tori gives it to the address), and the contacts found in the texts of private ads. Tori shows no private seller's name to visitors who are not logged in, so no name is delivered. Off: those fields are empty; a company's name, id, business id (Y-tunnus), address, phone and website are business data and always delivered. You are responsible for a lawful use, GDPR included.

## `markPaidPlacements` (type: `boolean`):

Read the paid ad Tori shows above each search's results (one small extra read per search, free) and flag each delivered row that is one (paidPlacement true or false); the search's coverage row names the paid ad and whether it stood among its results. A paid ad that is not among the results is not delivered (its own row cannot prove the search's filters). Ad links and shop stocks say null.

## `priceSummary` (type: `boolean`):

Each search's coverage row carries the count, lowest, quartiles, median, mean and highest asking price of the ads it met, and per Tori region and per category their count, median, lowest, highest and mean. Free.

## `includeRawData` (type: `boolean`):

Add Tori's search row as served (a private seller's only with personal data on).

## `descriptionFormat` (type: `string`):

How the ad description is delivered.

## `descriptionMaxLength` (type: `integer`):

Cut descriptions to this many characters (descriptionCut marks the row).

## `outputFields` (type: `array`):

Keep only these row fields, by the names the rows carry ("adId", "url", "region", "municipality", "companyName", "attributesById"); the row's identity, title, price, place, status and image count always stay. Not together with compactOutput.

## `compactOutput` (type: `boolean`):

Deliver only the most used fields (handy for AI agents).

## `excludeEmptyFields` (type: `boolean`):

Drop fields that are empty on a row.

## `extractContacts` (type: `boolean`):

Add the e-mail addresses, phone numbers, websites and social profiles written in the title and description (companies' ads always; private ads with includePersonalData on).

## `phoneExtractionMode` (type: `string`):

With extractContacts.

## `failIfIncomplete` (type: `boolean`):

End the run as failed when a search or link could not be read in full (the rows delivered stay).

## `maxFailedRequestsPercent` (type: `integer`):

End the run as failed when more of its reads failed.

## `completenessTolerancePercent` (type: `number`):

Your own bar for a complete search: a search counts as complete when it met at least Tori's count of ads less this percent (and less the number of ads below). Empty: the fixed rule (98 percent of Tori's count less 2 ads on a split search, 99 percent less 2 on one search).

## `completenessToleranceAbsolute` (type: `integer`):

Your own bar for a complete search, in ads: Tori's count less this many ads (and less the percent above). Empty: the fixed rule.

## `proxyConfiguration` (type: `object`):

Leave as is: Tori is read through Apify's datacenter proxy, which every Tori door answers. Your own proxy links replace it.

## `requestsPerSecond` (type: `number`):

The pace of the run's one exit (0.2 to 2).

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

At most this many requests in flight, inside the pace (Tori is never asked more than requestsPerSecond requests a second): ad pages are read on this many lanes.

## `maxRequestRetries` (type: `integer`):

Tries of a failed read after the first.

## Actor input object example

```json
{
  "keywords": [
    "sohva"
  ],
  "locations": [
    "Uusimaa"
  ],
  "sellerType": "any",
  "postedToday": false,
  "shippingOnly": false,
  "requireAllKeywords": false,
  "titleOnly": false,
  "onlyKeywordMatches": true,
  "maxItems": 20,
  "includeDetails": false,
  "includePersonalData": false,
  "markPaidPlacements": false,
  "priceSummary": true,
  "includeRawData": false,
  "descriptionFormat": "asServed",
  "compactOutput": false,
  "excludeEmptyFields": false,
  "extractContacts": false,
  "failIfIncomplete": false,
  "requestsPerSecond": 1,
  "maxConcurrency": 4,
  "maxRequestRetries": 2
}
```

# Actor output Schema

## `items` (type: `string`):

No description

## `report` (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 = {
    "keywords": [
        "sohva"
    ],
    "locations": [
        "Uusimaa"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation_craft/tori-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 = {
    "keywords": ["sohva"],
    "locations": ["Uusimaa"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation_craft/tori-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 '{
  "keywords": [
    "sohva"
  ],
  "locations": [
    "Uusimaa"
  ],
  "maxItems": 20
}' |
apify call automation_craft/tori-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation_craft/tori-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/Xe5QrvJCpeyoMQdIj/builds/lwbTHXRdQop5Lohpn/openapi.json
