← Blog

Vinted

Vinted Price Tracker: Build Your Own Vinted Price History

Track Vinted prices automatically: schedule a search, store daily snapshots, and spot price drops and market medians with a short Python and SQLite script.

Epic Scrapers

Vinted doesn’t show price history. You see today’s asking price and nothing else, so you can’t tell whether a seller just cut the price, whether a brand is getting cheaper, or what a fair price for a used item looks like this month.

A Vinted price tracker fixes that by running the same search on a schedule and keeping every result. This guide sets one up with the Vinted Scraper: built-in price-change fields for quick drop detection, plus a small Python script that stores snapshots in SQLite so you get real price history per listing and per search.

What you can track

  • Price drops on individual listings. A seller lowers a jacket from 45 to 35 EUR; you see it the next run.
  • Price history per listing. Every price a listing has had since you started watching.
  • Market prices for a search. Median and range of asking prices for, say, “Levi’s 501, size 32, very good condition”, day by day.
  • Supply. How many listings match a search each day, and how fast new ones appear.
  • Cross-country gaps. The same keyword’s median price per Vinted country over time.

These are asking prices, not sale prices. That’s still what a reseller needs to price stock and spot motivated sellers.

Price history is only meaningful if the search doesn’t change. Build it on Vinted with the filters that make listings comparable (brand, size, condition, category), copy the URL, and keep it fixed.

{
  "startUrls": [
    { "url": "https://www.vinted.fr/catalog?search_text=levis%20501&price_to=80&order=newest_first" }
  ],
  "maxProducts": 200,
  "trackPriceChanges": true
}

maxProducts matters more here than for alerts. The scraper only compares listings within the limit, so set it high enough to cover the whole search (up to 960). If a search has more matches than that, split it by price range or size.

Step 2: Use the built-in price-change fields

With trackPriceChanges set to true, the scraper remembers every listing it sees, per search, and adds these fields to each row:

Field Meaning
is_new_listing First time this search has seen the listing
first_seen_at When it was first seen
previous_price Price in the previous run
price_change Difference from the previous price, in the listing’s currency
price_change_percent The same, as a percentage
price_dropped true when the price is lower than last run

The first run is a baseline; changes show up from the second run on. In the Output tab, the Price tracking view shows previous prices and changes side by side.

If you only care about drops, set saveOnly to priceDrops and each run saves just the listings that got cheaper. That’s the lightest possible price tracker: schedule it, connect email in Integrations, done. (For instant Telegram or Discord pings, the Vinted alerts tutorial covers the notification side.)

The built-in fields compare against the previous run only. For a full history, store every run, which is the next step.

Step 3: Schedule it

Save the input as a task (Save as a new task), then go to Schedules in Apify Console and add it. Daily is the right cadence for most price tracking, and it keeps run costs low.

Use case Cron
Watchlist price drops 0 8 * * * (daily, 08:00)
Brand or category market prices 0 8 * * *
Cross-country comparison 0 8 * * 1 (weekly)

Step 4: Store snapshots and build price history

This script pulls the latest successful run of your task and appends one row per listing to SQLite, keyed by listing id and the run’s start time. Run it daily after the Apify schedule (cron, GitHub Actions, or any server).

import os, sqlite3
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run_client = client.task(os.environ["APIFY_TASK_ID"]).last_run(status="SUCCEEDED")
run = run_client.get()

db = sqlite3.connect("vinted_prices.db")
db.execute("""CREATE TABLE IF NOT EXISTS snapshots (
    run_id TEXT, seen_at TEXT, listing_id TEXT, title TEXT, url TEXT,
    brand TEXT, size TEXT, condition TEXT, country TEXT,
    price REAL, currency TEXT,
    PRIMARY KEY (run_id, listing_id))""")

for item in run_client.dataset().iterate_items():
    price = item.get("price") or {}
    if price.get("amount") is None:
        continue
    db.execute("INSERT OR IGNORE INTO snapshots VALUES (?,?,?,?,?,?,?,?,?,?,?)", (
        run["id"], str(run["startedAt"]), str(item["id"]),
        item.get("title"), item.get("url"), item.get("brand_title"),
        item.get("size_title"), item.get("status"), item.get("vinted_country"),
        float(price["amount"]), price.get("currency_code")))
db.commit()

The INSERT OR IGNORE on (run_id, listing_id) dedupes listings that appear twice in one run, and makes the script safe to re-run. price.amount arrives as a string, so it’s converted to a number here.

Use saveOnly: "all" for this task (the default). You want every listing in every snapshot, not only the changes, or your history will have gaps.

Listings that dropped since the previous snapshot, biggest first:

WITH ranked AS (
  SELECT listing_id, title, url, price, currency, seen_at,
         LAG(price) OVER (PARTITION BY listing_id ORDER BY seen_at) AS prev_price
  FROM snapshots)
SELECT title, prev_price, price, currency,
       ROUND(100.0 * (price - prev_price) / prev_price, 1) AS pct, url
FROM ranked
WHERE prev_price IS NOT NULL AND price < prev_price
  AND seen_at = (SELECT MAX(seen_at) FROM snapshots)
ORDER BY pct;

Full price history for one listing:

SELECT seen_at, price, currency FROM snapshots
WHERE listing_id = '1234567890' ORDER BY seen_at;

Daily market summary for the search, per condition:

SELECT date(seen_at) AS day, condition, currency,
       COUNT(*) AS listings, ROUND(AVG(price), 2) AS avg_price,
       MIN(price) AS min_price, MAX(price) AS max_price
FROM snapshots GROUP BY day, condition, currency ORDER BY day;

SQLite has no built-in median; load the table into pandas (df.groupby(["day", "condition"]).price.median()) if you want medians, which are less skewed by a few overpriced listings. Always group by currency: if you track several countries, GBP, PLN and EUR prices can’t be averaged together without converting.

Listings that were in yesterday’s snapshot but not today’s are gone from the search: sold, reserved, hidden or deleted. Counting them per day gives you a rough turnover signal for a niche.

Track prices across countries

For a keyword, add compareCountries and the scraper runs it on each Vinted site and saves a COUNTRY_PRICE_SUMMARY record with median prices per country converted to EUR. Read it from the run’s key-value store each week and append it to a table, and you have a cross-country price index:

summary = run_client.key_value_store().get_record("COUNTRY_PRICE_SUMMARY")["value"]
for s in summary:
    for c in s["countries"]:
        print(s["search"], c["country"], c["count"], c["median_eur"])

FAQ

Can I see the price history of a Vinted listing?

Not on Vinted itself. You have to record it, which is what the snapshot script does from the day you start.

How often should I check prices?

Daily for most searches. Hourly only makes sense for drop alerts on items that sell fast.

Can I track sold prices?

This scraper reads active listings, so it tracks asking prices. Disappearing listings hint at sales but don’t confirm them.

Do I need to code?

No, for drop alerts: saveOnly: "priceDrops" plus a schedule and an email integration is enough. Long-term history needs storage, either the script above or the Google Sheets integration appending each run.

Start tracking

Pick one search you know well, set trackPriceChanges to true, schedule it daily, and let it run for a week. Run the Vinted Scraper to set it up. To pull the data into other tools, see the Vinted API and export guide; for every field and search option, see the Vinted scraper guide.