# Motorkari Scraper (`gearshift-data/motorkari-scraper`) Actor

Fast motorcycle listing scraper for Motorkari.cz. Extract price, specs,
seller info, and optional descriptions/photos/phone. Supports filtering by
brand, model, year, mileage, price. Perfect for dealers, researchers, price
comparison tools.

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

## Pricing

from $8.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.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Motorkari.cz Scraper

Pulls motorcycle listings from [motorkari.cz](https://www.motorkari.cz/) and saves them as structured data you can open in Excel, Google Sheets, or feed into any other tool.

No coding needed — just set your filters and run.

***

### What you get

Each result is one motorcycle listing with price, brand, model, year, mileage, region, seller info, view count, and a main photo. Optionally you can also pull the full description, gallery photos, and the seller's phone number.

***

### Quick start

Just want to try it? This scrapes the first page of all listings with no filters:

```json
{
    "maxPagesPerCrawl": 1
}
```

***

### Ingest mode — **full** vs **incremental**

The actor has two modes, set by the `ingestMode` input. Default is `full`.

#### `full` (default)

Scrape the search results **and** fetch detail pages for every listing. Use this when you need a one-off complete dataset — exports, brand analysis, market snapshots.

```json
{ "ingestMode": "full", "manufacturer": "honda" }
```

#### `incremental`

Designed for repeated runs. The actor remembers which listing IDs it saw in previous runs (stored in its default key-value store as `seen-ids`) and uses that to do two things:

1. **Push a summary record for every listing found in the search results** — title, year, price, url. This lets you detect price changes across runs without refetching the detail page.
2. **Fetch full detail pages only for listings it hasn't seen before.** Known listings are skipped.
3. **Stop paginating when two consecutive search pages contain only known listings.** Avoids re-crawling the entire archive on daily runs.

```json
{ "ingestMode": "incremental", "maxPagesPerCrawl": 0 }
```

#### Do I need to run `full` first?

**No.** The first incremental run is its own bootstrap:

- `seen-ids` is empty → every listing is "new" → detail page is fetched for every listing, exactly like `full` mode.
- At the end, the actor saves all IDs it processed as `seen-ids`.
- The next incremental run only fetches details for listings that appeared since.

So: **pick a mode and stick with it.** Don't mix. If you run `full` once then `incremental`, the incremental bootstrap won't know about anything `full` scraped (different code path, doesn't write `seen-ids`). Just run `incremental` from day one.

#### What each run produces in `incremental` mode

Every dataset item carries a `scrapeStage` field:

- `summary` — lightweight record from the search listing row (title, year, price, url, id). Detail-only fields like `description`, `photos`, `phone` are `null`. Emitted for **every** listing the actor sees on the search pages.
- `detail` — full record with every field populated. Emitted only for listings not in `seen-ids`.

On a typical daily run: ~500 summary records (whole first page or two), ~5-50 detail records (actually-new listings).

#### Output types example (incremental)

Same listing, two records across two runs:

```json
// Run 1 — id is new → detail scrape
{ "scrapeStage": "detail", "id": "2048429", "title": "Husqvarna FE 450",
  "price": "25 000 Kč", "priceNumeric": 25000, "description": "...", "photos": [...] }

// Run 2 (one week later, seller dropped price) — id is known → summary only
{ "scrapeStage": "summary", "id": "2048429", "title": "Husqvarna FE 450",
  "price": "22 000 Kč", "priceNumeric": 22000, "description": null, "photos": [] }
```

Consumers of the dataset should treat `null` on `summary` records as "not available this run, don't overwrite". Price, title, year are present on both.

#### Running incremental on a schedule

Set up an Apify Schedule targeting this actor with input:

```json
{ "ingestMode": "incremental", "maxPagesPerCrawl": 0, "maxItems": 0, "scrapeDescription": true, "scrapePhotos": true }
```

Daily at the same time. Day-one run is the heaviest (full bootstrap). Subsequent runs scan the top few pages, fetch details for ~2-5% of listings, and stop once they hit known territory.

***

### Filters

All filters are optional. Mix and match what you need.

| Setting | What it does | Example |
|---------|-------------|---------|
| `customUrl` | Paste a search URL directly from motorkari.cz — skips all other filters | `"https://www.motorkari.cz/motobazar/motorky/honda/?..."` |
| `manufacturer` | Filter by brand (slug) | `"honda"`, `"yamaha"`, `"ktm"` |
| `model` | Filter by model (slug from the motorkari.cz URL, e.g. `"honda-cbr-600rr"`) | `"honda-cbr-600rr"` |
| `keywords` | Search for keywords in listings | `"ABS nové pneu"` |
| `minYear` / `maxYear` | Year range | `2018` / `2023` |
| `minPrice` / `maxPrice` | Price range in CZK | `50000` / `200000` |
| `minMileage` / `maxMileage` | Mileage range in km | `0` / `30000` |

***

### Optional extras

Off by default to keep things fast. Turn on what you need:

| Setting | What it does |
|---------|-------------|
| `scrapeDescription` | Pull the full listing description |
| `scrapePhotos` | Pull all gallery photo URLs |
| `scrapePhone` | Pull the seller's phone number |

> Enabling `scrapeDescription` and `scrapePhotos` together is no slower than enabling one alone — they come from the same detail page.

***

### Other settings

| Setting | Default | What it does |
|---------|---------|-------------|
| `ingestMode` | `full` | `full` or `incremental` — see section above |
| `maxPagesPerCrawl` | `1` | Stop after this many search pages. `0` = unlimited |
| `maxListingsPerPage` | `100` | Listings per page (40–100, step 10) |
| `maxItems` | `0` | Cap total items. `0` = unlimited |
| `datasetName` | *(auto)* | Named dataset instead of the run's default |
| `maxConcurrency` | `10` | Pages fetched in parallel |
| `timeoutSecs` | `600` | Max total run time in seconds |
| `proxyConfiguration` | Apify Proxy | Proxy settings |

***

### Examples

**KTMs from 2020 and up, full mode, with descriptions:**

```json
{
    "ingestMode": "full",
    "manufacturer": "ktm",
    "minYear": 2020,
    "scrapeDescription": true,
    "maxPagesPerCrawl": 3
}
```

**Daily incremental (set up as Apify Schedule):**

```json
{
    "ingestMode": "incremental",
    "maxPagesPerCrawl": 0,
    "scrapeDescription": true,
    "scrapePhotos": true
}
```

**Using a custom search URL from your browser:**

```json
{
    "customUrl": "https://www.motorkari.cz/motobazar/motorky/honda/?s[rok][0]=2019&s[cena][1]=200000"
}
```

***

### Sample output (detail stage)

```json
{
    "source": "motorkari",
    "date": "2026-02-27T10:15:44.123Z",
    "scrapeStage": "detail",
    "id": "2048429",
    "url": "https://www.motorkari.cz/motobazar/motorky/husqvarna/husqvarna-fe-450/husqvarna-fe-450-2048429.html",
    "sourceUrl": "https://www.motorkari.cz/motobazar/motorky/?pgr=1",
    "title": "Husqvarna FE 450",
    "brand": "husqvarna",
    "model": "husqvarna-fe-450",
    "year": 2020,
    "price": "25 000 Kč",
    "priceNumeric": 25000,
    "currency": "Kč",
    "orientacnePrice": null,
    "mileage": "5 000 km",
    "mileageNumeric": 5000,
    "region": "Praha",
    "seller": "Jan Novák",
    "sellerUrl": "https://www.motorkari.cz/bazar/user/12345/",
    "isCompany": false,
    "views": "123",
    "isTop": false,
    "isNew": false,
    "thumbnail": "https://www.motorkari.cz/images/bazar/husqvarna-fe-450-2048429.jpg",
    "photos": ["https://img.motorkari.cz/upload/images/.../1.jpg"],
    "photoCount": 1,
    "description": "Prodám motocykl v perfektním stavu...",
    "phone": null
}
```

***

### Output fields reference

| Field | Description |
|-------|-------------|
| `source` | Always `"motorkari"` |
| `date` | When this record was produced (ISO 8601) |
| `scrapeStage` | `"summary"` (search-listing data only) or `"detail"` (full record) |
| `id` | Listing ID on Motorkari.cz |
| `url` | Full URL to the listing |
| `sourceUrl` | Search results page the listing came from |
| `title` | Listing title |
| `brand` | Brand slug (e.g. `"honda"`) |
| `model` | Model slug |
| `year` | Year of manufacture (integer) |
| `price` | Price as displayed (`"25 000 Kč"`) |
| `priceNumeric` | Price as a plain number (`25000`) |
| `currency` | Currency symbol (`"Kč"`) |
| `orientacnePrice` | Approximate price if exact not given |
| `mileage` | Mileage as displayed (`"5 000 km"`) |
| `mileageNumeric` | Mileage as a plain number |
| `region` | Czech/Slovak region |
| `seller` | Seller's name |
| `sellerUrl` | Link to seller's profile |
| `isCompany` | `true` if seller is a business |
| `views` | Listing view count |
| `isTop` | Featured/TOP listing |
| `isNew` | New-badge on the listing |
| `thumbnail` | Main photo URL |
| `photos` | Full gallery URLs *(when `scrapePhotos: true`)* |
| `photoCount` | Number of photos |
| `description` | Full listing description *(when `scrapeDescription: true`)* |
| `phone` | Seller's phone number *(when `scrapePhone: true`)* |

***

### 💰 Pricing

This actor uses Apify's **Pay-Per-Result model**:

- **$8 per 1,000 results** scraped
- First 500 results free to try each month
- No charge if the actor fails or finds no results
- Cancel anytime — no long-term contracts

Incremental mode is much cheaper to run on a schedule — after the first day, daily runs typically return a few hundred summary records and fewer than 50 detail records.

# Actor input Schema

## `ingestMode` (type: `string`):

• Full — scrape search results and fetch details for every listing.
• Incremental — only fully scrape listings that weren't seen in previous runs. The actor remembers IDs in its key-value store and uses them to skip already-known listings. Summary records (title, price, url, year) are still produced for every listing so you can detect price changes.

## `customUrl` (type: `string`):

Paste a complete Motorkari.cz search URL with your pre-configured filters. If provided, all filter fields below will be ignored.

## `manufacturer` (type: `string`):

Filter by manufacturer (slug format). Examples: ktm, honda, yamaha, bmw, kawasaki, suzuki, harley-davidson.

## `model` (type: `string`):

Filter by model (format: brand-model-name). Examples: ktm-125-smc-r, honda-cb-500.

## `minYear` (type: `integer`):

Minimum year of manufacture. Leave empty for no lower bound.

## `maxYear` (type: `integer`):

Maximum year of manufacture. Leave empty for no upper bound.

## `minMileage` (type: `integer`):

Minimum mileage in kilometres. Leave empty for no lower bound.

## `maxMileage` (type: `integer`):

Maximum mileage in kilometres. Leave empty for no upper bound.

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

Minimum price in Czech koruna. Leave empty for no lower bound.

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

Maximum price in Czech koruna. Leave empty for no upper bound.

## `keywords` (type: `string`):

Search keywords within listings (e.g. ABS, serviced).

## `scrapeDescription` (type: `boolean`):

Extract full listing text from detail pages using fast HTTP requests.

## `scrapePhotos` (type: `boolean`):

Extract full-size gallery photos from detail pages (typically 4-8 images per listing).

## `scrapePhone` (type: `boolean`):

Extract seller's phone number from the listing's detail page.

## `archivePhotos` (type: `boolean`):

Download each listing's gallery photos, resize them to WebP, and upload to a Cloudflare R2 bucket. Protects against photo loss when source listings end. Adds `photosArchived[]` to the output. Requires the R2 fields below.

## `r2AccountId` (type: `string`):

Cloudflare account id — the subdomain of your R2 S3 endpoint (https://<account>.r2.cloudflarestorage.com).

## `r2BucketName` (type: `string`):

Target R2 bucket.

## `r2AccessKeyId` (type: `string`):

R2 API token access key id. Use Apify secrets to avoid storing credentials in the Task input.

## `r2SecretAccessKey` (type: `string`):

R2 API token secret. Use Apify secrets.

## `r2PublicBaseUrl` (type: `string`):

Public URL prefix for archived photos, e.g. https://pub-abc123.r2.dev (shown in R2 bucket settings after enabling public access).

## `r2MaxDimension` (type: `integer`):

Resize each photo so its longer edge fits within this many pixels. 1200 ≈ 200 KB WebP per photo.

## `r2Quality` (type: `integer`):

WebP encoder quality. 80 is a good trade-off between size and fidelity.

## `maxPagesPerCrawl` (type: `integer`):

Maximum number of search result pages to process. Set to 0 for unlimited.

## `maxListingsPerPage` (type: `integer`):

Listings per search page (40-100, step 10). Higher = fewer pages but slower per page.

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

Total listings to scrape. Set to 0 for unlimited.

## `datasetName` (type: `string`):

⚠️ Using a named dataset makes the run's default dataset appear empty (data stored in the named one instead).

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

Apify Proxy is recommended for reliable scraping.

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

How many times to retry a failed HTTP request before giving up.

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

Pages processed simultaneously. 5-15 recommended.

## `timeoutSecs` (type: `integer`):

Maximum total runtime before the actor aborts.

## `debugLog` (type: `boolean`):

Enable verbose debug logging for troubleshooting. Noisy — leave off for production runs.

## Actor input object example

```json
{
  "ingestMode": "full",
  "customUrl": "https://www.motorkari.cz/motobazar/motorky/?s[cat]=2&s[rok][0]=1990&s[rok][1]=2000",
  "minYear": 2000,
  "maxYear": 2025,
  "scrapeDescription": false,
  "scrapePhotos": false,
  "scrapePhone": false,
  "archivePhotos": false,
  "r2BucketName": "gearshift-moto-photos",
  "r2MaxDimension": 1200,
  "r2Quality": 80,
  "maxPagesPerCrawl": 1,
  "maxListingsPerPage": 100,
  "maxItems": 0,
  "datasetName": "motorkari-honda-2026",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxRequestRetries": 3,
  "maxConcurrency": 10,
  "timeoutSecs": 600,
  "debugLog": false
}
```

# Actor output Schema

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

All scraped listings showing key fields: title, brand, year, price, mileage, location, and link.

## `bestDeals` (type: `string`):

Listings sorted by price (lowest first), then mileage (nulls last), then year (newest first).

## `lowestMileage` (type: `string`):

Listings sorted by mileage (lowest first, nulls last), then price, then year.

## `newestFirst` (type: `string`):

Listings sorted by year (newest first), then price, then mileage.

## `detailed` (type: `string`):

All fields including seller info, pricing details, and metadata.

## `withDescriptions` (type: `string`):

Listings that include extracted text descriptions (only populated when scrapeListingText is enabled).

## `withImages` (type: `string`):

Listings that include photo gallery images (only populated when scrapeDetailImages is enabled).

# 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 = {
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("gearshift-data/motorkari-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 = { "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    } }

# Run the Actor and wait for it to finish
run = client.actor("gearshift-data/motorkari-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 '{
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call gearshift-data/motorkari-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,gearshift-data/motorkari-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/34PKV0NXDWpg0Qm63/builds/bQ7tSgsSGlZBXBBQt/openapi.json
