Vinted Scraper
Pricing
from $1.40 / 1,000 results
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
Maintained by CommunityActor 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.
Session cookie
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):
- downloads the homepage once per domain (through the normal downloader, so proxies apply) and stores every cookie;
- attaches the cookies as a
Cookieheader to all requests to that domain; - absorbs
Set-Cookieheaders from every response, so rotated tokens are picked up; - 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:
- 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, ...);
- price – the range is bisected at the median price seen on page 1 (
price_from/price_toare inclusive, so the halves are[lo, m]and[m+0.01, hi]); - condition – one query per
status_idsvalue; - colour – one query per
color_idsvalue (items without a colour can be missed here, hence last); - 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
mode | What it collects | Section to fill |
|---|---|---|
search | products matching keywords (optionally inside given categories) | searchQueries (+ categoryIds, filters) |
category | every product of the given categories, past the 960 cap | categoryIds (+ filters) |
category_tree | the whole category tree (~2 900 categories) as type: "category" records | – |
items | full details of specific products | itemUrls (URLs or numeric IDs) |
urls | products from arbitrary catalog URLs copied from the browser, filters included | startUrls |
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
| Field | Description |
|---|---|
mode | search / category / category_tree / items / urls (see above) |
countries | List 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 |
searchQueries | Keywords, one per line; with categoryIds every keyword is searched in every category |
categoryIds | Category ids (e.g. 1904 Women, 5 Men, 1193 Kids, 1918 Home, 2994 Electronics, 2525 duffle coats) |
itemUrls | Item URLs (https://www.vinted.fr/items/123-slug) or numeric item IDs |
startUrls | Catalog URLs with any filters, or item URLs, from any Vinted domain |
priceFrom, priceTo, order, brands, brandIds, statusIds, colorIds, sizeIds, materialIds | Filters, identical to the parameters in Vinted's URLs; brands are names resolved through the brand API |
fetchItemDetails | Visit every item page (≈ +1 request / product); always on in items mode |
fetchSellerDetails | Call the seller API for every product (small JSON request) |
maxItems | Stop after N unique products (0 = unlimited) |
splitCappedResults, maxSplitDepth | Cap-splitting controls (see above) |
maxConcurrency, downloadDelay, proxyConfiguration | Crawl politeness and proxies |
browserImpersonation | curl_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¤cy=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.txtpython -m src# as a plain Scrapy spiderscrapy crawl vinted -a actor_input='{"mode":"search","countries":["fr"],"searchQueries":["nike dunk"],"maxItems":200}' -O out.json# on Apifyapify 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, Dockerfilesrc/__main__.py `python -m src` entry point (Apify + Twisted reactor bootstrap)src/main.py Actor main: input -> Scrapy settings -> crawlsrc/settings.py Scrapy settingssrc/middlewares.py VintedSessionMiddleware (homepage cookie), VintedBlockMiddleware (DataDome/Cloudflare),VintedImpersonateMiddleware (curl_cffi browser fingerprint)src/spiders/vinted.py The spider: queries, splitting, pagination, details, sellers, categoriessrc/vinted/countries.py Country -> domain / currencysrc/vinted/rsc.py React Server Components payload decodingsrc/vinted/parsers.py Catalog / item / category / seller parserssrc/vinted/query.py Query model, URL parsing, split strategy