Vinted Scraper avatar

Vinted Scraper

Pricing

from $1.40 / 1,000 results

Go to Apify Store
Vinted Scraper

Vinted Scraper

Scrape Vinted listings across multiple countries. Search by keywords, browse categories, or fetch specific items and URLs. Apply price, brand, size and condition filters, enrich results with extra details, and export clean data to JSON, CSV or Excel.

Pricing

from $1.40 / 1,000 results

Rating

0.0

(0)

Developer

9839_ser

9839_ser

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

0

Monthly active users

15 hours ago

Last modified

Categories

Share

Vinted Scraper (Apify Actor, Scrapy)

Scrapes Vinted on any of its 25 country domains:

  • keyword search (mode: search), optionally combined with filters and categories;
  • whole categories (mode: category), including categories far above Vinted's 960-item result cap;
  • the full category tree (mode: category_tree, ~2 900 categories per country);
  • specific items (mode: items) by URL or ID;
  • any Vinted catalog URL (mode: urls), the domain of the URL selects the country;
  • optional full item details (description, attributes, all photos, breadcrumbs, seller rating) and seller profiles (city, country, ratings, item counts, last seen).

Built with Scrapy and the Apify Python SDK.

How it works

Vinted's web app is a Next.js application. Its old public JSON catalog API (/api/v2/catalog/items) returns 404 for non-app clients, so the scraper reads the React Server Components payload embedded in every server-rendered /catalog and /items/<id> page (src/vinted/rsc.py, src/vinted/parsers.py). Each catalog page carries 96 items, the pagination block and the complete category tree. Responses are gzip/brotli compressed, so a catalog page costs roughly 0.5 MB of traffic.

The homepage of each domain hands out an anonymous session (access_token_web, a 24h JWT, plus refresh_token_web and a few others). VintedSessionMiddleware (src/middlewares.py):

  1. downloads the homepage once per domain (through the normal downloader, so proxies apply) and stores every cookie;
  2. attaches the cookies as a Cookie header to all requests to that domain;
  3. absorbs Set-Cookie headers from every response, so rotated tokens are picked up;
  4. refreshes the session 10 minutes before the JWT expires and immediately on a 401, retrying the failed request.

Scrapy's own cookie middleware is disabled; this middleware is the single source of truth. The JSON endpoints that need the token are the seller profile (/api/v2/users/<id>) and brand lookup (/api/v2/brands).

Getting past the 960-item cap

Vinted never returns more than 10 pages x 96 = 960 items for a single query, and total_entries is capped at 960 as well. A query whose first page reports 960 entries is therefore treated as capped and split into disjoint sub-queries (src/vinted/query.py::split_query), in this order:

  1. categories – several ids become one query per id; a single id is replaced by its child categories; a query without a category is split into the root categories (Women, Men, Kids, Home, ...);
  2. price – the range is bisected at the median price seen on page 1 (price_from/price_to are inclusive, so the halves are [lo, m] and [m+0.01, hi]);
  3. condition – one query per status_ids value;
  4. colour – one query per color_ids value (items without a colour can be missed here, hence last);
  5. sort order – the four sort orders are fetched and deduplicated (overlapping, best effort).

Every piece that reports fewer than 960 entries is paginated normally. Products are deduplicated by id across all sub-queries. maxSplitDepth bounds the recursion; queries that still hit the cap are logged as "truncated" at the end of the run (vinted/queries_truncated in the stats).

Blocks

Vinted sits behind DataDome and Cloudflare and rate-limits a single IP after a few dozen fast requests. VintedBlockMiddleware recognises 403/429 responses and challenge pages served with 200 and retries them with exponential backoff (2 s, 4 s, 8 s, ... up to 60 s, Retry-After honoured). A 429 also puts the whole domain into a cool-down so new requests wait instead of piling up more 429s. With Apify Proxy every retry leaves through a different IP. Use residential proxies for anything beyond a few hundred requests.

Browser fingerprint (curl_cffi)

DataDome also fingerprints the TLS/HTTP2 handshake. Item pages fetched with Scrapy's Twisted downloader get a 403 straight away, independent of the IP. All requests therefore go through scrapy-impersonate (curl_cffi), which impersonates a real browser's handshake and header set. VintedImpersonateMiddleware tags every request with the target from browserImpersonation (default chrome) and drops Scrapy's own User-Agent/Accept so the headers match the impersonated browser. Set browserImpersonation to none to fall back to the plain Twisted downloader.

Modes

modeWhat it collectsSection to fill
searchproducts matching keywords (optionally inside given categories)searchQueries (+ categoryIds, filters)
categoryevery product of the given categories, past the 960 capcategoryIds (+ filters)
category_treethe whole category tree (~2 900 categories) as type: "category" records–
itemsfull details of specific productsitemUrls (URLs or numeric IDs)
urlsproducts from arbitrary catalog URLs copied from the browser, filters includedstartUrls

countries selects one or more Vinted marketplaces. In the search, category and category_tree modes every query runs on every selected country. In the items and urls modes the domain inside each URL decides the country; a bare item ID is opened on the first selected country (item IDs are global).

Input

FieldDescription
modesearch / category / category_tree / items / urls (see above)
countriesList of country codes: de, fr, uk, us, es, it, nl, be, pl, cz, lt, at, lu, pt, sk, se, ro, hu, hr, dk, fi, gr, ie, ee, lv
searchQueriesKeywords, one per line; with categoryIds every keyword is searched in every category
categoryIdsCategory ids (e.g. 1904 Women, 5 Men, 1193 Kids, 1918 Home, 2994 Electronics, 2525 duffle coats)
itemUrlsItem URLs (https://www.vinted.fr/items/123-slug) or numeric item IDs
startUrlsCatalog URLs with any filters, or item URLs, from any Vinted domain
priceFrom, priceTo, order, brands, brandIds, statusIds, colorIds, sizeIds, materialIdsFilters, identical to the parameters in Vinted's URLs; brands are names resolved through the brand API
fetchItemDetailsVisit every item page (≈ +1 request / product); always on in items mode
fetchSellerDetailsCall the seller API for every product (small JSON request)
maxItemsStop after N unique products (0 = unlimited)
splitCappedResults, maxSplitDepthCap-splitting controls (see above)
maxConcurrency, downloadDelay, proxyConfigurationCrawl politeness and proxies
browserImpersonationcurl_cffi browser fingerprint: chrome (default), edge, safari, firefox, none

Condition ids: 6 new with tags, 1 new without tags, 2 very good, 3 good, 4 satisfactory.

Quick examples (each finishes in well under a minute)

{ "mode": "urls", "countries": ["de"], "maxItems": 500,
"startUrls": [
{ "url": "https://www.vinted.de/catalog/2525-duffle-coats?price_from=1000&currency=EUR&order=newest_first" },
{ "url": "https://www.vinted.fr/catalog/1773-capes-and-ponchos?brand_ids[]=53&order=newest_first" },
{ "url": "https://www.vinted.co.uk/catalog?search_text=carhartt%20detroit%20jacket&status_ids[]=6&order=newest_first" }
] }
{ "mode": "search", "countries": ["pl", "it"], "searchQueries": ["levis 501"], "statusIds": ["6"], "maxItems": 150 }
{ "mode": "items", "countries": ["de"], "fetchSellerDetails": true,
"itemUrls": ["https://www.vinted.fr/items/10418177392-baskets-nike-dunk", "10418692724"] }
{ "mode": "category_tree", "countries": ["de", "fr"] }

A full category export is the expensive case: { "mode": "category", "countries": ["fr"], "categoryIds": ["2525"], "maxItems": 0 } walks the whole duffle-coat category (tens of thousands of items) with the price / condition / colour splits.

Output

All records go to the default dataset and carry a type field. The dataset schema with field descriptions and two Console views (Products, Categories) is in .actor/dataset_schema.json; the Actor output schema (.actor/output_schema.json) exposes the products view, the categories view, the raw dataset and a CSV export in the run's Output tab.

type: "product"

{
"type": "product",
"id": 10418177392,
"url": "https://www.vinted.de/items/10418177392-baskets-nike-dunk",
"title": "Baskets Nike Dunk",
"brand": "Nike",
"sizeTitle": "44",
"condition": "Zufriedenstellend",
"price": 22.0,
"currency": "EUR",
"priceWithDiscount": null,
"serviceFee": 1.8,
"totalPrice": 23.8,
"favouriteCount": 0,
"isPromoted": false,
"thumbnailUrl": "https://images1.vinted.net/t/.../310x430/....webp",
"photos": ["https://images1.vinted.net/t/.../f800/....webp"],
"dominantColor": "#C8C2BB",
"seller": { "id": 29648461, "isBusiness": false },
"country": "de",
"source": { "searchText": null, "catalogIds": [1242], "priceFrom": 20, "priceTo": 24.5, "statusIds": [], "colorIds": [], "brandIds": [], "order": "newest_first", "page": 3 },
"scrapedAt": "2026-10-17T20:11:54+00:00"
}

With fetchItemDetails the record additionally contains description, attributes (brand, size, status, colour, material, ... as shown on the page), color, material, brandId, uploadedAgo, breadcrumbs, categoryPath, categoryId, canBuy, instantBuy, isReserved, isHidden, all photos, shippingPrice, buyerProtectionFee, a pricing object (item price, buyer-protection fee, shipping price with original price / discount / free flag, pick-up-only flag, total amount, as shown to an anonymous visitor of that domain), structuredData (JSON-LD: category, colour, condition, availability) and seller.name, seller.feedbackCount, seller.feedbackReputation. There is no separate request for shipping: the pricingServices block is part of the same server-rendered payload. View counts are not public on Vinted item pages any more and therefore not available.

With fetchSellerDetails the seller object gains name, profileUrl, city, countryCode, feedbackCount, feedbackReputation, positive/neutral/negative counts, itemCount, totalItemsCount, followersCount, lastLoggedOnTs, isOnHoliday, about, verification.

type: "category"

{ "type": "category", "id": 2525, "title": "Dufflecoats", "path": "Damen > Kleidung > Jacken & Mäntel > Mäntel > Dufflecoats",
"parentId": 1907, "parentTitle": "Mäntel", "depth": 4, "url": "https://www.vinted.de/catalog/2525-duffle-coats",
"isLeaf": true, "childrenIds": [], "photoUrl": null, "country": "de", "scrapedAt": "..." }

Titles are localised per domain; ids and URL slugs are the same across countries.

Running

# locally (input in storage/key_value_stores/default/INPUT.json, same format as on Apify)
python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt
python -m src
# as a plain Scrapy spider
scrapy crawl vinted -a actor_input='{"mode":"search","countries":["fr"],"searchQueries":["nike dunk"],"maxItems":200}' -O out.json
# on Apify
apify push

Cost estimate

One catalog page (96 products) is a single ~0.5 MB request. A category of 50 000 items therefore needs about 550 catalog requests plus the page-1 probes of the splits (typically 10-20 % overhead). fetchItemDetails adds one ~0.4 MB request per product; fetchSellerDetails adds one ~4 KB request per product.

Project layout

.actor/ Actor manifest, input schema, output schema, dataset schema, Dockerfile
src/__main__.py `python -m src` entry point (Apify + Twisted reactor bootstrap)
src/main.py Actor main: input -> Scrapy settings -> crawl
src/settings.py Scrapy settings
src/middlewares.py VintedSessionMiddleware (homepage cookie), VintedBlockMiddleware (DataDome/Cloudflare),
VintedImpersonateMiddleware (curl_cffi browser fingerprint)
src/spiders/vinted.py The spider: queries, splitting, pagination, details, sellers, categories
src/vinted/countries.py Country -> domain / currency
src/vinted/rsc.py React Server Components payload decoding
src/vinted/parsers.py Catalog / item / category / seller parsers
src/vinted/query.py Query model, URL parsing, split strategy