# Vinted Scraper - Listings with Favourite Counts (`ziomixshot/vinted-scraper`) Actor

Collect verified Vinted listings from 26 marketplaces with favourite counts, search, category, brand, size, condition and price filters, and monitor new listings and favourite changes.

- **URL**: https://apify.com/ziomixshot/vinted-scraper.md
- **Developed by:** [Amadeusz](https://apify.com/ziomixshot) (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.00 / 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.

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

## Vinted Scraper

Collect listings from [Vinted](https://www.vinted.com) with the number of favourites (`favouriteCount`) of every listing. Pick a marketplace (26 countries), give a phrase, a category, brands, sizes, condition and a price range, or paste a search URL copied from Vinted. Every listing is checked against your filters, de-duplicated and returned as clean JSON, ready for CSV, Excel, the API or AI agents.

[Run Vinted Scraper](https://console.apify.com/actors/glRL8DUxSwZdv90rW?addFromActorId=glRL8DUxSwZdv90rW) · [Input schema](https://apify.com/ziomixshot/vinted-scraper/input-schema) · [Output schema](https://apify.com/ziomixshot/vinted-scraper/output-schema)

### What you get

- Favourite counts: `favouriteCount` in every record, the same number the listing page shows.
- 26 marketplaces: `pl`, `fr`, `de`, `it`, `es`, `nl`, `be`, `lt`, `cz`, `sk`, `at`, `pt`, `hu`, `ro`, `hr`, `se`, `dk`, `fi`, `lu`, `gr`, `co.uk`, `com`, `ie`, `lv`, `ee`, `si`.
- Names instead of IDs: `category` takes a path (`Mężczyźni > Obuwie > Obuwie sportowe`), `brands` takes brand names, `conditions` takes names (`new_with_tags`, `new_without_tags`, `very_good`, `good`, `satisfactory`).
- Filters you can discover: a free run with `listFilters` lists every filter of a category with its allowed values and the input field to put them in.
- Price range, four sort orders, search URLs copied from Vinted (`startUrls`), several searches in one run with shared de-duplication.
- Verified results: filters are checked against what Vinted reports as applied. A filter that Vinted would silently ignore stops the run with an explanation instead of returning wrong data.
- No duplicates: the same listing is returned once per run, and you are charged once.
- Optional details (`enrichDetails`): description, full photo list, category path, attributes, color, how long ago the listing was posted, seller profile and reputation.
- Monitoring (`mode: monitor`): each run returns only listings not reported by earlier runs; with `trackFavourites` it also returns a `change` record when the favourite count of a listing moved.
- Free estimate (`countOnly`): how many listings match your search, without scraping or charges.
- More than 960 results per search: one Vinted search exposes at most 960 listings, so the Actor splits large searches by price automatically.
- One date format (UTC, `YYYY-MM-DDTHH:mm:ssZ`) in every field.

### Quick start

1. Choose a **Marketplace** and type a **Search phrase**, or choose a **Category**.
2. Add brands, condition, price range or other filters if you need them.
3. Set **Max items** (the limit of listings returned and charged).
4. Click **Start**. The default input (`country: pl`, `maxItems: 100`) is a working example.
5. Open the **Overview** table, export JSON, CSV or Excel, or read the data through the API.

Not sure how many listings match? Start with **Count only**: it is free.

### Pricing

Pay per event, no start fee:

| Event | Price | When |
|---|---|---|
| `listing` | $1.00 / 1000 | One per listing returned, with `favouriteCount` |
| `listing-details` | $4.00 / 1000 | One per listing when `enrichDetails` is on and the details were fetched |
| `listing-change` | $0.50 / 1000 | One per `change` record (`trackFavourites`) |

Duplicates, rejected cards, empty runs, `countOnly` and `listFilters` runs and runs that end with an input or filter error are not charged. `maxItems` and your maximum cost per run are hard limits: the run stops when either is reached.

Cost of a run = listings returned × $0.001 (plus $0.004 per enriched listing and $0.0005 per change record). Run `countOnly` first to see how many listings match.

### Ready-to-use recipes

#### 1. Search by phrase on one marketplace

```json
{
  "country": "pl",
  "query": "nike air max",
  "priceTo": 200,
  "sortBy": "newest",
  "maxItems": 100
}
```

#### 2. Category, brand and condition

```json
{
  "country": "fr",
  "category": "Hommes > Chaussures > Baskets",
  "brands": ["Nike", "Adidas"],
  "conditions": ["new_with_tags", "very_good"],
  "maxItems": 300
}
```

Run `listCategories` to find the exact category name or ID, and `listFilters` with the same `country` and `category` to see every filter and value of the category.

#### 3. Search URL copied from Vinted

```json
{
  "startUrls": ["https://www.vinted.de/catalog?search_text=levis&order=newest_first"],
  "maxItems": 200
}
```

The marketplace comes from the URL. A parameter the Actor does not know is rejected instead of ignored.

#### 4. Monitor new listings and favourite changes

```json
{
  "country": "pl",
  "query": "iphone 13",
  "mode": "monitor",
  "trackFavourites": true,
  "seenStoreName": "vinted-iphone",
  "maxItems": 200
}
```

Schedule it in Apify. The first run returns what is there; later runs return only new listings and `change` records.

#### Use it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/ziomixshot~vinted-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN"   -H "Content-Type: application/json"   -d '{"country":"pl","query":"nike air max","maxItems":50}'
```

The same Actor is available to AI agents through the [Apify MCP server](https://mcp.apify.com): add `ziomixshot/vinted-scraper` to the tools of the server.

### Input

The full description of every field is in the input form. A field the Actor does not know (for example a typo) is rejected when the run starts.

| Field | Description |
|---|---|
| `country` | Marketplace code, default `pl`. Ignored for `startUrls` (the URL decides). |
| `query` | Search phrase. |
| `category` | Category name or path in the language of the marketplace (`Kobiety > Ubrania > Sukienki`), a category code or a category page URL. A name used by several categories is refused with the list to choose from. |
| `brands` | Brand names, matched to the Vinted brand with the same name (or the best match); the run log shows what was chosen. |
| `conditions` | `new_with_tags`, `new_without_tags`, `very_good`, `good`, `satisfactory`. |
| `priceFrom`, `priceTo` | Price range in the currency of the marketplace, inclusive. |
| `sortBy` | `relevance`, `newest`, `priceAsc` or `priceDesc`. A price order starts from the cheapest (or most expensive) listing of the whole result, so combine `priceAsc` with `priceFrom` to skip very cheap ones. |
| `maxItems` | Hard limit of returned listings and charges, shared by all `startUrls` (default 100). |
| `includePromoted` | Vinted mixes promoted cards into results; `false` skips them (default `true`). |
| `enrichDetails` | Fetch the listing page of each listing; each one also triggers the `listing-details` event. |
| `includeRawData` | Add `raw`, the unchanged Vinted card of the listing. |
| `mode`, `seenStoreName`, `seenIdsKey`, `stopMonitorOnAllSeenPages`, `trackFavourites` | Monitoring, see below. |
| `countOnly` | Return only the number of matching listings (free). |
| `listFilters` | Return the filters of the category instead of listings (free). |
| `listCategories` | Return the category tree of the marketplace (ID, path, code, URL) instead of listings (free); `category` narrows it to paths containing that text. |
| `startUrls` | Search URLs copied from Vinted, as strings or `{url}` objects. They replace all search fields above. |
| `categoryId`, `brandIds`, `sizeIds`, `colorIds`, `materialIds` | Vinted numeric IDs, for when you already know them. `listFilters` shows them. |
| `otherFilters` | Filters of the category without a field of their own, as filter code to a list of IDs, for example `{"video_game_platform":[7]}`. `listFilters` lists them; a filter the category does not have fails the run before any listing is charged. |

`category` and `categoryId` cannot be combined, and neither can `brands` and `brandIds`; the run stops with an explanation instead of guessing.

### Output

Every listing is one dataset item. The dataset has an `Overview` view, and the **Overview** table also shows `status`, `estimate`, `filter`, `category` and `change` records.

Fields of every `listing` item: `recordType`, `id`, `url`, `title`, `country`, `price`, `currency`, `priceWithDiscount`, `serviceFee` (buyer protection), `totalPrice`, `favouriteCount`, `brand` (as shown on the card), `size` and `condition` (text in the language of the marketplace), `isPromoted`, `thumbnailUrl`, `photos` (card photos), `seller` (`id`, `isBusiness`), `scrapedAt`. A value Vinted does not provide is `null`.

Added by `enrichDetails`: `detailsFetched`, `description`, `originalPrice`, `categoryId`, `categoryPath`, `brandId`, `sizeId`, `color`, `attributes`, `uploadedAgo`, `isReserved`, `isHidden`, `canBuy`, all photos, and in `seller`: `login`, `feedbackCount`, `feedbackReputation` (0 to 1), `lastLoggedIn` (relative text, for example `3 godz.`), `badges`. The `favouriteCount` of a detailed record is read from the listing page, so it is the freshest value. A listing Vinted no longer serves is skipped and not charged; the count is `detailsUnavailable` in the run status.

Other record types, always free except `change`:

- `status`: the run ended because of your input and returned no listings. Fields: `status`, `message` (what is wrong and how to fix it), `details`, `scrapedAt`.
- `estimate`: result of `countOnly`: `estimatedTotal`, `isLowerBound`, `sources`, `note`.
- `filter`: result of `listFilters`: `code`, `title`, `selectionType`, `inputField`, `values`, `note`.
- `category`: result of `listCategories`: `id`, `path`, `title`, `code`, `url`, `hasSubcategories`, `country`. Put `id` into `categoryId`, or `path` into `category`.
- `change`: see monitoring.

#### Run status

The `OUTPUT` record of the key-value store describes the run: `status` (`OK`, `PARTIAL`, `INVALID_INPUT`, `URL_NOT_FULLY_RESOLVED`, `FILTER_VERIFICATION_FAILED`, `SEEN_STATE_INVALID`, `UPSTREAM_ERROR`), counters and `jobs[]` per search (`criteria` as sent to Vinted, `expectedTotal`, `coverage`, `partitions`, `truncatedPartitions`, `failedPages`). Input and filter errors end the run without listings and without charges. `PARTIAL` means some pages or details failed after all retries; the returned records are still valid.

### Monitoring new listings and favourite changes

`mode: monitor` returns only listings whose ID was not reported by earlier runs with the same `seenStoreName` and `seenIdsKey` (a named key-value store in your account). Without `seenStoreName` the Actor uses a store named after the search (`vinted-monitor-...`, shown in the run log), so repeating the same search keeps its history; set a name to share one history between different runs. The first run returns everything up to `maxItems`; later runs return only new IDs.

- It always reads newest first and ignores `sortBy`, does not split the search and skips promoted cards.
- It reads pages until `stopMonitorOnAllSeenPages` consecutive pages hold nothing new.
- When `maxItems` stops it before that, the listings it did not return stay unseen and the next run returns them (the run log says so). Raise `maxItems` to catch up in one run.
- State is saved only for listings that were actually returned. A corrupted state record ends the run with `SEEN_STATE_INVALID` instead of starting over.

#### Tracking favourites (`trackFavourites`)

With `trackFavourites: true` the run also returns a `change` record for every listing whose `favouriteCount` changed since the previous run: `recordType: "change"`, `metric: "favourites"`, `id`, `url`, `title`, `country`, `previous`, `current`, `delta` and `changedAt`. New listings are still returned as `listing` items.

- It compares the cards the run reads anyway (the newest listings of the search, up to the first page without new IDs), with no extra requests. A listing that falls off those pages is not checked.
- The first time a listing is seen only a baseline counter is stored; a change needs a stored value to compare with.
- Counters are stored next to the seen IDs, in the record named like `seenIdsKey` plus `_FAVOURITES` (up to 200000 newest listings).
- `maxItems` limits listings and change records together. A change beyond the limit is kept for the next run.

### Estimate (`countOnly`)

Returns one item with `recordType: "estimate"` and `estimatedTotal`. One Vinted search reports at most 960 matches, so a result of 960 is a lower bound (`isLowerBound: true`, the run message says "at least 960"): narrow the filters to see the real number. Counting beyond 960 would mean reading the search page by page, which is what a run does. With several `startUrls` the result is the sum of the searches. It is not charged.

### Limits and notes

- One search exposes at most 960 listings (10 pages of 96). For `maxItems` above that the Actor splits by price. Sorting then applies only inside each part. `truncatedPartitions` above 0 means more than 960 listings with the same price that cannot be separated.
- Vinted shows no absolute posting date on the listing page: `enrichDetails` returns the relative text as `uploadedAgo` (for example `2 dni`). Vinted shows no public view counter, so there is none in the output; the favourite count is the only public counter.
- `price` is in the currency of the marketplace; Vinted converts listings from other countries. `originalPrice` (with `enrichDetails`) is the price as the seller listed it.
- Names of categories and conditions are in the language of the marketplace. A category name that is not found is answered with close names; `listCategories` shows the whole tree.
- A filter code in `otherFilters` that the category does not offer stops the run before anything is charged, with the codes the category has.
- `maxItems` is one budget for the whole run: with several `startUrls` the first URLs use it up first.
- Vinted protects its site with anti-bot systems. Runs use Apify residential proxies in the country of the marketplace (included in the platform usage cost). A page that cannot be read after all retries is counted in `failedPages` and the run ends `PARTIAL`.
- The Actor uses the web interface of Vinted, which is unofficial and can change without notice.
- The Actor reads public pages anonymously. It never logs in, never favourites, messages or buys, and never changes anything on an account.

### Legal notice

This Actor collects data that Vinted shows publicly. Vinted's terms of service restrict automated access to the site and the use of its content; you are responsible for using the data lawfully and in line with the terms of the marketplace you read. Listings contain personal data of sellers (login, ID, reputation), so GDPR rules apply to how you store and use them. The Actor is not affiliated with Vinted.

### Development

`npm test`, `npm run typecheck`, `npm run lint`. The mapping of the Vinted network requests lives in `docs/openapi/` (`openapi.yaml`, `ENDPOINTS.md`, `captures/`); `npm run openapi` regenerates it from the Playwright captures in `tmp/har/` (`npm run openapi:capture`). Backlog: [docs/backlog.md](docs/backlog.md), architecture: [docs/diagram.md](docs/diagram.md), competitors and pricing: [docs/pricing.md](docs/pricing.md).

# Actor input Schema

## `query` (type: `string`):

For example nike air max. Leave empty to browse a whole category or brand.

## `country` (type: `string`):

The Vinted marketplace to search; the run reads it through a residential proxy of the same country.

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

Category name or path in the language of the marketplace, for example Mężczyźni > Obuwie > Obuwie sportowe, a category code or a category page URL. A name used by several categories is refused with the list to choose from. Not sure of the name? Run with Show categories to list every category of the marketplace with its ID. Show category filters lists the filters of a category.

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

Brand names, for example Nike. Each name is matched to the Vinted brand with the same name (or the best match); the run log shows what was chosen.

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

Only listings in one of these conditions.

## `priceFrom` (type: `number`):

Minimum price, inclusive, in the currency of the marketplace.

## `priceTo` (type: `number`):

Maximum price, inclusive, in the currency of the marketplace.

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

Relevance, newest, or by price. Price order starts from the cheapest or most expensive listing in the whole result, so set Price from to skip very cheap ones. Monitor mode always reads the newest first.

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

Hard limit of unique listings returned and charged, shared by all Search URLs of the run (they are read one after another). Vinted returns at most 960 listings per search, so a larger limit splits the search by price.

## `includePromoted` (type: `boolean`):

Promoted listings match your filters too. Turn off to return organic results only.

## `enrichDetails` (type: `boolean`):

Open every listing page and add the description, all photos, category path, attributes, relative upload time and seller details. Each enriched record is also charged the listing-details event; listings Vinted no longer serves are skipped and not charged.

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

Add the unmodified catalog card as raw.

## `mode` (type: `string`):

Scrape returns every matching listing. Monitor returns only listing IDs not reported by earlier runs that use the same store and key; it reads the newest first, skips promoted cards and ignores the sort option.

## `seenStoreName` (type: `string`):

Named key-value store shared by monitor runs. Leave empty to use a store named after the search (vinted-monitor-...; the run log shows it), so repeating the same search keeps its history. Set a name to share one history between different runs.

## `seenIdsKey` (type: `string`):

Record in the store that holds the reported IDs (up to 500000 newest IDs are kept).

## `stopMonitorOnAllSeenPages` (type: `integer`):

Monitor stops when this many consecutive pages contain no new organic listing. Raise it when new listings may appear further down the list.

## `trackFavourites` (type: `boolean`):

Monitor only. Also returns a change record for every listing whose favourite count changed since the previous run. It compares the cards on the pages the monitor reads; the first run only stores the counters. Counters are kept next to the seen IDs, in the record named like the seen-IDs key plus \_FAVOURITES (up to 200000 newest listings). Charged as the listing-change event.

## `countOnly` (type: `boolean`):

Return only the number of listings Vinted reports for the search, without collecting or charging for listings. Vinted never reports more than 960 per search, so a larger search is reported as "at least 960". Filters are still verified.

## `listFilters` (type: `boolean`):

List the filters of the chosen category (codes, allowed values with IDs and the input field that takes them) instead of collecting listings. Needs Category or Category ID. Free.

## `listCategories` (type: `boolean`):

List the category tree of the marketplace (ID, path, code, URL) instead of collecting listings. Category narrows the list to paths containing that text. Free.

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

Copied Vinted catalog URLs (vinted.pl, vinted.fr, ...), one per line. When set, they replace Search phrase, Category, Brands and the other search fields, so fill either the URLs or the fields. A URL with a parameter this Actor does not know is rejected.

## `categoryId` (type: `integer`):

Vinted category ID, for example 1452. Use it instead of Category when you already know the ID.

## `brandIds` (type: `array`):

Vinted brand IDs, for example \[53]. Added to Brands.

## `sizeIds` (type: `array`):

Vinted size IDs. Show category filters lists them.

## `colorIds` (type: `array`):

Vinted colour IDs. Show category filters lists them.

## `materialIds` (type: `array`):

Vinted material IDs. Show category filters lists them.

## `otherFilters` (type: `object`):

Filters of the category without a field of their own, as filter code to a list of IDs, for example {"video\_game\_platform":\[7]}. Show category filters lists them. A filter the category does not have fails the run before any listing is charged.

## Actor input object example

```json
{
  "query": "nike air max",
  "country": "pl",
  "category": "Kobiety > Obuwie",
  "brands": [
    "Nike",
    "Adidas"
  ],
  "sortBy": "relevance",
  "maxItems": 100,
  "includePromoted": true,
  "enrichDetails": false,
  "includeRawData": false,
  "mode": "scrape",
  "seenIdsKey": "VINTED_SEEN_IDS",
  "stopMonitorOnAllSeenPages": 1,
  "trackFavourites": false,
  "countOnly": false,
  "listFilters": false,
  "listCategories": false,
  "startUrls": [
    "https://www.vinted.pl/catalog?search_text=nike"
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

Listings found by the run, one dataset item per listing. A run that ended on your input holds one status record with the reason; countOnly holds one estimate record.

## `filters` (type: `string`):

Filters of the category, from a run with Show category filters.

## `categories` (type: `string`):

Category tree of the marketplace, from a run with Show categories.

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

Listings whose favourite count changed, from a monitor run with Track favourite counts.

## `status` (type: `string`):

Status record with counters (cards read, duplicates, rejected cards, failed pages) and the error code when the input or filters were invalid.

# 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 = {
    "query": "nike air max",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("ziomixshot/vinted-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 = {
    "query": "nike air max",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("ziomixshot/vinted-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 '{
  "query": "nike air max",
  "maxItems": 100
}' |
apify call ziomixshot/vinted-scraper --silent --output-dataset

```

## MCP server setup

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