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.
Vinted
Track Vinted prices automatically: schedule a search, store daily snapshots, and spot price drops and market medians with a short Python and SQLite script.
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.
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.
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.
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) |
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.
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"])
Not on Vinted itself. You have to record it, which is what the snapshot script does from the day you start.
Daily for most searches. Hourly only makes sense for drop alerts on items that sell fast.
This scraper reads active listings, so it tracks asking prices. Disappearing listings hint at sales but don’t confirm them.
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.
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.