A normal Vinted search tells you what exists right now.
A reseller usually needs the opposite:
What changed since yesterday?
That sounds like a small difference, but it changes the whole scraper design.
One-off exports are easy. Scheduled market monitoring is not. You need stable inputs, a persistent state store, clean diffs, cost caps, and a way to avoid calling every missing item "sold" when Vinted simply changed ranking or hid a page.
I ran this live on Apify on June 4, 2026 with Vinted Smart Scraper.
Disclosure: I built this Actor. Some Apify links may use my referral code.
π The live baseline
Before changing the article, I pulled the real public Store and run data.
| Metric | Value |
|---|---|
| Actor | kazkn/vinted-smart-scraper |
| Actor ID | 4UAPrwZb1XOlkCJKK |
| Supported Vinted markets | 26 |
| Countries per run cap | 5 |
| Total public runs | 108,367 |
| Total users | 464 |
| Users, 7 days | 31 |
| Users, 30 days | 97 |
| Runs, 30 days | 7,392 |
| Succeeded runs, 30 days | 7,166 |
| Rating | 5.0 / 5 |
| Reviews | 4 |
Current Free-tier event pricing at the time of the test:
$0.020 per Actor start
$0.002 per dataset result
So the billing guardrail is simple:
estimated charge = actor start + emitted rows * result price
β The wrong abstraction
My first mistake was thinking like this:
"Users need a Vinted scraper."
That is too broad.
Resellers do not only need raw listings. They need a repeatable signal.
| β One-time scraper | β Market monitor |
|---|---|
| Export one CSV | Run the same input every day |
| Count active listings | Compare snapshots |
| Trust hidden sold pages | Track missing, likely_sold, reappeared
|
| Pull everything | Cap rows with maxItems
|
| Pay for noisy rows | Emit only changes after baseline |
| Debug manually | Send summary rows to CSV/API/webhook |
That is why the most important mode is not the normal SEARCH.
It is SELL_THROUGH_TRACKER.
π§ͺ Run 1: normal search
I started with a tiny real search:
{
"mode": "SEARCH",
"query": "nike air force 1",
"countries": ["fr"],
"condition": ["very_good", "good"],
"priceMin": 20,
"priceMax": 120,
"sortBy": "newest_first",
"maxItems": 8,
"includePhotos": false,
"includeSellerDetails": false
}
Result:
| Field | Value |
|---|---|
| Run ID | 7BHS0mBNeN4faoDZk |
| Dataset ID | DQGMfYNvIQabnV2wF |
| Status | SUCCEEDED |
| Duration | 10s |
| Rows emitted | 8 |
| Total available in Vinted response | 960 |
| Free-tier estimated charge | $0.036 |
Sample output:
{
"id": 9089776821,
"title": "Nike air force 1 shadow unisex numero 45",
"url": "https://www.vinted.fr/items/9089776821-nike-air-force-1-shadow-unisex-numero-45",
"price": 30,
"currency": "EUR",
"brand": "Air Force",
"size": "45",
"condition": "Bon Γ©tat",
"country": "fr",
"seller": {
"id": 3160438769,
"username": "eeeeeeed1"
}
}
That is useful, but it is still just a snapshot.
The more interesting question is:
What happens if I run the same thing again?
β Run 2: build a sell-through baseline
Here is the baseline input:
{
"mode": "SELL_THROUGH_TRACKER",
"query": "nike air force 1",
"countries": ["fr"],
"condition": ["very_good", "good"],
"priceMin": 20,
"priceMax": 120,
"sortBy": "newest_first",
"maxItems": 8,
"trackingStoreName": "devto-series-sell-through-2026-06-04",
"trackerId": "devto-nike-air-force-fr-2026-06-04",
"missingRunsThreshold": 2,
"emitActiveItems": true,
"emitRunSummary": true
}
The important fields:
| Field | Why it matters |
|---|---|
trackingStoreName |
Keeps state across scheduled runs |
trackerId |
Stable ID for this monitor |
missingRunsThreshold |
Reduces false sold signals |
emitActiveItems |
Full baseline first, lighter output later |
emitRunSummary |
Adds one daily summary row for automations |
Live result:
| Field | Value |
|---|---|
| Run ID | gCd8a3kk4AC9yJWpI |
| Dataset ID | 0QbscZ6GRdhmOlSqx |
| Status | SUCCEEDED |
| Duration | 6s |
| Rows emitted | 9 |
| Item rows | 8 |
| Summary rows | 1 |
| Free-tier estimated charge | $0.038 |
The summary row is what turns the Actor into a daily monitor:
{
"recordType": "SELL_THROUGH_DAILY_SUMMARY",
"trackerId": "devto-nike-air-force-fr-2026-06-04",
"isBaseline": true,
"scheduleReady": true,
"observedItems": 8,
"outputItemRecords": 8,
"totalTrackedItems": 8,
"newItems": 8,
"activeItems": 8,
"missingItems": 0,
"likelySoldItems": 0,
"explicitSoldItems": 0,
"sellThroughRate": 0,
"avgPrice": 35,
"topBrands": [
{ "brand": "Air Force", "count": 5 },
{ "brand": "Nike", "count": 3 }
]
}
The baseline does not prove anything sold yet.
It creates the memory needed for the next run.
π Run 3: same tracker, lighter scheduled output
For the second run, I kept the same trackingStoreName and trackerId, but changed one thing:
{
"emitActiveItems": false
}
Full input:
{
"mode": "SELL_THROUGH_TRACKER",
"query": "nike air force 1",
"countries": ["fr"],
"condition": ["very_good", "good"],
"priceMin": 20,
"priceMax": 120,
"sortBy": "newest_first",
"maxItems": 8,
"trackingStoreName": "devto-series-sell-through-2026-06-04",
"trackerId": "devto-nike-air-force-fr-2026-06-04",
"missingRunsThreshold": 2,
"emitActiveItems": false,
"emitRunSummary": true
}
Live result:
| Field | Value |
|---|---|
| Run ID | sbCksMapWGd5GBG9O |
| Dataset ID | 55gRHqbUbiqnmT6O3 |
| Status | SUCCEEDED |
| Duration | 5s |
| Rows emitted | 2 |
| Item rows | 1 |
| Summary rows | 1 |
| Free-tier estimated charge | $0.024 |
That second run found:
{
"recordType": "SELL_THROUGH_DAILY_SUMMARY",
"isBaseline": false,
"observedItems": 8,
"outputItemRecords": 1,
"totalTrackedItems": 9,
"newItems": 1,
"activeItems": 8,
"missingItems": 1,
"likelySoldItems": 0,
"explicitSoldItems": 0,
"newlyMissingItems": 1,
"dailySellThroughSignals": 0,
"avgPrice": 34.38
}
One item was not observed on the second run:
{
"recordType": "SELL_THROUGH_ITEM",
"itemId": 9089774198,
"title": "Nike Air Pegasus Volt Green Neon - EUR42 - HQ5403-700",
"trackingStatus": "missing",
"changeType": "missing",
"confidence": "low",
"lastSeenPrice": 39.99,
"missingRuns": 1,
"missingRunsThreshold": 2
}
Notice the important part:
missingRuns: 1
missingRunsThreshold: 2
confidence: low
likelySoldItems: 0
The Actor did not call this sold after one missing run.
That is intentional.
π§ Why I do not trust sold pages blindly
I also ran SOLD_ITEMS on the same query:
{
"mode": "SOLD_ITEMS",
"query": "nike air force 1",
"countries": ["fr"],
"maxItems": 5
}
Live result:
| Field | Value |
|---|---|
| Run ID | wvFbmO0HscQits0UV |
| Status | SUCCEEDED |
| Rows emitted | 0 |
| Free-tier estimated charge | $0.020 |
The log showed the reason:
Sold-items endpoint variants returned active listings only.
Returning no rows instead of false sold items.
That is exactly the kind of boring defensive behavior I want.
| Situation | Bad scraper behavior | Safer behavior |
|---|---|---|
| Vinted returns active listings from a sold endpoint | β Mark active items as sold | β Return zero rows |
| Item disappears for one run | β Claim it sold | β
Mark missing, low confidence |
| Item stays missing across runs | β Increase confidence | β
Use missingRunsThreshold
|
πΌοΈ Snapshot diff visual
The mental model is:
baseline snapshot
β
scheduled snapshot
β
diff engine
β
daily summary row + item-level changes
For a webhook or Telegram bot, the useful object is not every listing.
It is the summary:
{
"telegramText": "Vinted sell-through devto-nike-air-force-fr-2026-06-04: 8 observed, 1 first seen, likely sold: 0, sold: 0, reappeared: 0."
}
πΈ Cost guardrails
With current Free-tier event pricing:
start = $0.020
row = $0.002
The test charges would be:
| Run | Rows | Estimated Free-tier charge |
|---|---|---|
| Search snapshot | 8 | $0.036 |
| Sell-through baseline | 9 | $0.038 |
| Scheduled second pass | 2 | $0.024 |
| Sold explicit check | 0 | $0.020 |
The practical rule:
| Workflow | First run | Scheduled run |
|---|---|---|
| Inspect all current inventory | β
emitActiveItems: true
|
β usually too noisy |
| Daily changes only | β not enough baseline | β
emitActiveItems: false
|
| Webhook / Telegram | β
keep emitRunSummary: true
|
β
keep emitRunSummary: true
|
Start with maxItems: 100.
Only move to 300 after one run proves the query is useful.
β οΈ Limits
This is not magic.
| Claim | Reality |
|---|---|
| "Missing means sold" | β No. Missing can mean hidden, removed, reranked, moderated, or sold. |
| "Sold pages are always reliable" | β Not in my tests. Some endpoint variants returned active listings. |
| "More countries is always better" | β Current cap is 5 countries per run for stability. |
| "Schedule everything hourly" | β You pay per start and per emitted row. Start daily. |
| "This guarantees profit" | β It only gives better market data. |
π Try the exact monitor
Open Vinted Smart Scraper on Apify.
Use this first:
{
"mode": "SELL_THROUGH_TRACKER",
"query": "nike air force 1",
"countries": ["fr"],
"maxItems": 100,
"trackingStoreName": "daily-nike-air-force-fr",
"trackerId": "daily-nike-air-force-fr",
"missingRunsThreshold": 2,
"emitActiveItems": true,
"emitRunSummary": true
}
Run it once.
Then save it as an Apify Task, add a Schedule, and switch:
{
"emitActiveItems": false
}
That is the difference between scraping Vinted once and monitoring a resale market every day.
π Series
This is Part 1 of Vinted Smart Scraper - Market Monitor.
Next:
- Part 2: Cross-Country Vinted Search: Comparing 5 Markets Without Burning Sessions
- Part 3: Seller Watchlists, Price Drops, and Webhooks: The Vinted Signals I Actually Schedule
- Part 4: Best Vinted Scraper in 2026: An Honest Developer Comparison

Top comments (0)