# Tori.fi Ads Scraper (`axiomworks/tori-fi-ads-scraper`) Actor

Get structured data for public ads in Tori.fi search results, including IDs, titles, locations, seller types, trade labels, publication dates, images, and listing URLs. Search with keywords or use Tori.fi URLs to preserve filters; duplicate ads are removed across pages and searches.

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

## Pricing

from $1.48 / 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

## Tori.fi Ads Scraper

**Returns:** one row per Tori.fi ad with id, title, price, currency, location, postedAt, images and sourceUrl.
**Input:** `{"searchQueries": ["polkupyora"], "minPrice": 50, "maxPrice": 150, "maxItems": 30}`
**Best for:** price monitoring of second-hand goods, reseller sourcing lists, local market research.

```json
{
  "id": "48437849",
  "title": "Miesten hybrid",
  "price": 200,
  "currency": "EUR",
  "location": "Kaleva, Tampere",
  "sourceUrl": "https://www.tori.fi/recommerce/forsale/item/48437849"
}
```

### What does Tori.fi Ads Scraper do?

Tori.fi Ads Scraper collects public classified advertisements from Tori.fi search results. Give it Finnish search keywords or a Tori.fi search URL, and it returns one structured record per advertisement. Each record has a stable ad ID and a link to its public listing. The Actor reads the public search page, including the page's embedded search data and structured data. No login is required.

The Actor follows Tori.fi search pagination, up to the site's first 50 pages for each input search. It fetches pages sequentially at a modest rate. It stops as soon as `maxItems` valid records have been emitted. The number of results depends on the public ads visible when the run starts, the query, filters, and the site's paging limit.

This is an independent tool. It is not affiliated with Tori.fi. The fields reflect the public search response, so they may differ from a listing's later edited detail page. If you need full descriptions or seller contact details, those are outside this Actor's output.

### What data can you get?

The dataset contains one row for each unique public ad. Empty optional values are represented by `null`, and an ad with no images has an empty array. Title, location, ID, and source URL are required for a record to be emitted.

| Field | Meaning |
| --- | --- |
| `id` | Stable Tori.fi advertisement ID. |
| `title` | Heading shown in search results. |
| `price` | Numeric asking price when present. |
| `currency` | Currency code attached to the price. |
| `location` | Place label shown with the ad. |
| `sellerType` | `private` or `business` when the search flags identify it. |
| `tradeType` | The site's sale or trade label, such as `Myydään`. |
| `latitude` | Latitude supplied in search data; null when the ad has no map position. |
| `longitude` | Longitude supplied in search data. |
| `postedAt` | Publication time converted to an ISO date string. |
| `images` | Public image URLs included with the search result. |
| `condition` | Product condition from the page's structured data. |
| `organisationName` | Business name on retailer ads; null for private sellers. |
| `brand` | Brand set on the ad, when the seller provided one. |
| `attributes` | Extra attributes from the ad, keyed by attribute id with a list of values, or `null`. |
| `category` | Optional coarse classification from TypeSafe AI. |
| `sourceUrl` | Canonical public listing URL. |

The search data supplies the ID, heading, price, location, seller flag, trade type, coordinates, timestamp, image URLs, and canonical URL. The page's JSON-LD structured data supplies `condition` when the matching ad appears there. When either source does not include an optional value, the record keeps `null` rather than guessing it. The `images` array contains URLs; the Actor does not download or store the image files.

If `TYPESAFE_API_KEY` is configured for the run, TypeSafe AI's Jev model classifies the title into a broad `category`: electronics, vehicles and parts, home and furniture, clothing and fashion, sports and leisure, or other. This enrichment uses a third-party AI processor. It sees the ad title for classification. A classification can be absent if the processor is unavailable or the run reaches its AI call limit. The source fields remain useful without the enrichment.

Posting-date filters support repeat market samples without changing the output format. Set `postedAfter` to a UTC calendar date such as `2026-10-01`, and optionally set `postedBefore` to the last day to include. Both bounds include their named day. Empty strings disable the bounds. Ads without a timestamp are excluded when a date bound is active. Tori's timestamp can reflect a listing refresh; it is not a guarantee of first publication. Date filtering checks each returned ad and follows normal pagination, rather than assuming promoted results are in strict chronological order.

Set `sellerType` to `private` for household listings or `business` for ads carrying the site's retailer flag. The default `all` preserves the existing results. A filtered run only counts matching records toward the shared `maxItems` limit, and may fetch more search pages to find enough matches. Unknown seller flags are excluded from a specific seller selection. Combine these controls with the existing price bounds or URL filters for a narrower sample.

### How to use

Enter at least one keyword in `searchQueries`, or provide one or more public Tori.fi search pages in `startUrls`. The two methods can be used together. Keywords are appropriate for a broad search. A search URL is useful when you have already chosen filters on Tori.fi and want the Actor to retain those URL parameters.

Set `maxItems` to the maximum number of unique ads to return across all inputs. The Actor considers the inputs in order: search URLs first, then keyword searches. For each input it walks pages in ascending page order. It skips any ad ID already emitted earlier in the same run. If fewer matching public ads exist, the run returns fewer records. A nonsense term yields an empty dataset with a warning.

`sort` controls keyword searches when the URL has no explicit sort parameter. The choices are relevance, newest publication, lowest price, and highest price. A supplied search URL can carry its own sort and other site filters. If a URL already contains `sort`, the Actor leaves it intact. Page numbers in a supplied URL establish where that input begins; subsequent pages increment from there.

Direct access is used by default. If public Tori.fi pages block a run, enable Apify Proxy using `proxyConfiguration`. The Actor retries block responses and rate limits with backoff and a fresh proxy session for each attempt. It never solves a CAPTCHA. A target that cannot obtain a usable public response is skipped with a warning; other targets continue. If no ads are returned, the successful run status suggests broader keywords, fewer price filters, a current search URL, or proxy settings. Missing targets or malformed inputs still fail validation.

The search pages are requested one at a time, at roughly one request per second. This pace helps avoid burdening the site. It also means a large result limit can take longer than a small one. The prefilled run collects at most 30 ads.

### Input

| Input | Type | What to provide |
| --- | --- | --- |
| `searchQueries` | Array of strings | Finnish keywords such as `polkupyora`; use this or `startUrls`. |
| `startUrls` | Array of URL objects | Public Tori.fi search URLs, including any selected filters. |
| `sort` | Enum | `RELEVANCE`, `PUBLISHED_DESC`, `PRICE_ASC`, or `PRICE_DESC`; default `PUBLISHED_DESC`. |
| `minPrice` | Integer | Only ads priced at or above this amount in EUR. Ads without a price are excluded when set. Optional. |
| `maxPrice` | Integer | Only ads priced at or below this amount in EUR. Optional. |
| `postedAfter` | String | Optional inclusive lower UTC date, YYYY-MM-DD; empty by default. |
| `postedBefore` | String | Optional inclusive upper UTC date, YYYY-MM-DD; empty by default. |
| `sellerType` | Enum | `all` (default), `private`, or `business`, based on source flags. |
| `maxItems` | Integer | Maximum unique ads, from 1 to 10000; default 50. |
| `proxyConfiguration` | Object | Optional Apify Proxy settings; direct access by default. |
| `maxJevCalls` | Integer | Maximum enrichment and fallback judgments, from 0 to 500; default 500. |

Here is a keyword run with a strict result limit:

```json
{
  "searchQueries": ["polkupyora"],
  "sort": "PUBLISHED_DESC",
  "maxItems": 30,
  "sellerType": "private",
  "postedAfter": "2026-10-01"
}
```

Here is a search URL run. The URL can include Tori.fi filters selected in a browser:

```json
{
  "startUrls": [
    {"url": "https://www.tori.fi/recommerce/forsale/search?q=iphone"}
  ],
  "maxItems": 60
}
```

A URL must point to the public `/recommerce/forsale/search` route on `www.tori.fi` or `tori.fi`. Other target sites and listing detail URLs are outside this Actor's input scope. Search terms are sent as query parameters with proper URL encoding. A start URL is preserved when it advances to later pages, apart from its `page` value. Inputs that contain neither a usable keyword nor a search URL fail promptly with a clear message.

For an optional proxy run, set `proxyConfiguration.useApifyProxy` to `true` and, if needed, specify the supported Apify proxy group or country fields. The Actor asks Apify for a proxy URL on each retry, so blocked requests can use a fresh session. A normal direct run does not require a proxy configuration.

### Output

The following real item came from a local sofa search using both date bounds and the private-seller filter. Listings may change after collection.

```json
{
  "id": "48459265",
  "title": "Harmaa sohva",
  "price": 150,
  "currency": "EUR",
  "location": "Sipoo, Sipoo",
  "sellerType": "private",
  "tradeType": "Myydään",
  "latitude": 60.35936,
  "longitude": 25.27203,
  "postedAt": "2026-10-13T12:43:40.000Z",
  "images": [
    "https://img.tori.net/dynamic/default/item/48459265/16a153ce-807d-41f8-b80a-a6643474afb9",
    "https://img.tori.net/dynamic/default/item/48459265/6d03e75b-25eb-49ea-8226-dfbee596109f"
  ],
  "condition": "UsedCondition",
  "brand": "IKEA",
  "attributes": {
    "brand": [
      "IKEA"
    ]
  },
  "category": null,
  "sourceUrl": "https://www.tori.fi/recommerce/forsale/item/48459265"
}
```

### How much does it cost?

The Actor is charged per result. Each emitted ad counts as a `result`. An `enriched-result` event is recorded only when the optional AI category was actually produced. Setting a smaller `maxItems` bounds the number of emitted results. Check the Actor's Pricing tab for current event terms and rates; the tab is the source of truth as pricing can change.

A run that finds no ads emits no result records. Retried page requests do not themselves create extra output records. The Actor deduplicates ad IDs before pushing data, so an ad repeated on a later search page is still returned only once. It stops before pushing an item beyond `maxItems`.

### Use with AI agents (MCP)

Apify's MCP server can expose this Actor as a tool to an AI agent. Configure the server URL with this Store path so the agent can discover the input schema and start a run:

```text
https://mcp.apify.com/?tools=axiomworks/tori-fi-ads-scraper
```

An agent should supply either `searchQueries` or `startUrls` and an appropriate `maxItems`. The output dataset gives it stable IDs and source URLs, allowing it to refer back to the public ads it discusses. Examples of grounded requests an agent can make are:

> Find up to 20 public Tori.fi bicycle ads for `polkupyora` and compare their asking prices and locations.

> Collect up to 30 public Tori.fi iPhone listings from this search URL and return their titles, prices, and listing links.

> Show which ads in a query have coordinates and which have image URLs, without filling missing fields from guesses.

### Integrations and API

Python example using the Apify client:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("axiomworks/tori-fi-ads-scraper").call(
    run_input={"searchQueries": ["polkupyora"], "maxItems": 20}
)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
for item in items:
    print(item["id"], item["title"], item["sourceUrl"])
```

JavaScript example using the Apify client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('axiomworks/tori-fi-ads-scraper').call({
  searchQueries: ['polkupyora'],
  maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => process.stdout.write(`${item.id} ${item.title}\n`));
```

A cURL call can start a run with JSON input. Replace the token placeholder in your own environment; do not put credentials in a public script or repository:

```bash
curl -X POST \
  'https://api.apify.com/v2/acts/axiomworks~tori-fi-ads-scraper/runs' \
  -H 'Authorization: Bearer YOUR_APIFY_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"searchQueries":["polkupyora"],"maxItems":20}'
```

### FAQ

**Can I use several keywords?** Yes. Put several strings in `searchQueries`. The Actor processes them in order and removes duplicate ad IDs within the run. `maxItems` applies to the combined output, so the first search can consume the entire limit if it contains enough ads.

**Can I search a category or price range?** Yes. Select filters on a public Tori.fi search page and pass the resulting search URL in `startUrls`. The Actor retains the URL's query parameters while paging. It does not reinterpret the site's category IDs or price filters.

**Why are some optional fields null?** A public search result may omit coordinates, price, seller flags, or a structured condition. The Actor preserves missing values as null. It does not infer exact location, currency, seller type, or condition from unrelated words.

**Why is the AI category null?** The run may have no TypeSafe AI key, reach `maxJevCalls`, or receive an error from the processor. In these cases, the Actor continues with the source fields and does not record an enriched result. Classification is limited to broad product categories.

**Can the Actor fetch more than 50 search pages?** It follows the public search paging metadata with a maximum of 50 pages per input. Narrow filters or several search URLs can help collect a targeted sample. The Actor never invents additional pages beyond the site's reported range.

**Does it visit detail pages?** No. It reads public search pages and their embedded data. `sourceUrl` points to the detail page for a person or another workflow to inspect, but this Actor does not retrieve descriptions or seller contact information from that page.

**What happens with an invalid URL or term?** Unsupported URLs fail validation quickly. A valid search with no results finishes with zero dataset items and a warning. A blocked page is retried a few times with backoff, then the target is skipped. If all targets are unavailable, the run succeeds with zero items and advice to change the search or connection.

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

Public listing data can be subject to applicable law, site terms, and rights held by sellers or the platform. Check the rules relevant to your use, especially if you intend to republish data or retain it over time. This Actor reads public search pages, uses a modest request rate, and does not access account-only content. It does not solve CAPTCHAs or collect seller contact details. You remain responsible for how you use and store the output.

### Feedback

If a field stops appearing, a page begins returning a block, or a search filter behaves unexpectedly, report the search input, run ID, and a short description of the observed result through the Actor's Apify feedback channel. Avoid posting credentials or private data. A reproducible public URL and the affected field help distinguish a site layout change from a query with no current advertisements.

# Actor input Schema

## `searchQueries` (type: `array`):

Search keywords as strings, e.g. \["polkupyora", "sohva"]. Supply searchQueries or startUrls; both are accepted together. URLs run first, then keywords. Empty searches return zero ads; try broader Finnish terms.

## `startUrls` (type: `array`):

Public HTTPS Tori.fi search URLs as objects, e.g. \[{"url":"https://www.tori.fi/recommerce/forsale/search?q=iphone"}]. Use the /recommerce/forsale/search path, not an ad detail URL or ID. URL filters and starting page are retained. Supply this or searchQueries.

## `sort` (type: `string`):

Choose RELEVANCE, PUBLISHED_DESC (newest, default), PRICE_ASC (lowest price), or PRICE_DESC (highest price). Applies to searches without a sort URL parameter; an explicit URL sort takes priority.

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

Include only ads priced at least this EUR amount, e.g. 50. Omit or use null for no lower bound. Applied to every target after site filters; ads without prices are excluded when either price bound is set.

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

Include only ads priced at most this EUR amount, e.g. 500. Omit or use null for no upper bound. Applied to every target after site filters; narrow price bounds can return zero ads.

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

Include ads posted on or after this UTC date, YYYY-MM-DD, e.g. 2026-10-01. Empty (default) disables the lower date bound. Uses postedAt from search data; excludes undated ads when enabled. Filtering may return fewer than maxItems.

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

Include ads through the end of this UTC date, YYYY-MM-DD. Empty (default) disables the upper date bound. Combine with postedAfter for an inclusive calendar-date window. The site timestamp can reflect a refreshed listing.

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

Keep all sellers (default), only private sellers, or only business sellers. Uses the source private/retailer flags already reported in sellerType. Unknown seller types are excluded when selecting private or business; applies across every target.

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

Return at most this many unique ads across all URLs and keywords combined, 1-10000, default 50; e.g. 30. Fewer ads are returned when filters or public availability limit matches. This is not a per-search limit.

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

Network settings, e.g. {"useApifyProxy":true}. Default {"useApifyProxy":false} requests public pages directly. Enable Apify Proxy if targets are blocked; retries use fresh sessions. Unavailable targets return zero ads with advice to change the search or connection.

## `maxJevCalls` (type: `integer`):

Cap TypeSafe AI category classification and missing-field fallback calls across the run, 0-500, default 500. Set 0 to disable AI calls. Requires a configured runtime AI key; without one category remains null. Source fields are extracted without AI.

## Actor input object example

```json
{
  "searchQueries": [
    "polkupyora"
  ],
  "sort": "PUBLISHED_DESC",
  "postedAfter": "",
  "postedBefore": "",
  "sellerType": "all",
  "maxItems": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "maxJevCalls": 500
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset of collected ads

# 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 = {
    "searchQueries": [
        "polkupyora"
    ],
    "sort": "PUBLISHED_DESC",
    "maxItems": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiomworks/tori-fi-ads-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 = {
    "searchQueries": ["polkupyora"],
    "sort": "PUBLISHED_DESC",
    "maxItems": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("axiomworks/tori-fi-ads-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 '{
  "searchQueries": [
    "polkupyora"
  ],
  "sort": "PUBLISHED_DESC",
  "maxItems": 30
}' |
apify call axiomworks/tori-fi-ads-scraper --silent --output-dataset

```

## MCP server setup

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