Documentation
Last updated 2026-11-08
Base URL https://swanum.com/comps/api/v1 · JSON over HTTPS · openapi.json · examples on GitHub
Short answer: Comps is a REST API that returns what items actually sold for on Vinted (France, Germany, UK, Italy, Spain, Netherlands, Poland): median, quartiles, time to sell and the individual sold items, as JSON. Vinted has no official API and no sold-items search; we track listings and record a sale only when the item page says “Sold”. It is free during the beta, 1,000 requests a month, sign up with just an email.
Quickstart (under 2 minutes)
- Sign up with your email — no password, no card. Your first key is shown once on the dashboard.
- Make a call:
export COMPS_KEY="sc_live_..."
curl -G https://swanum.com/comps/api/v1/vinted/sold \
-H "X-API-Key: $COMPS_KEY" \
--data-urlencode "q=nike air force 1" -d country=FRYou get statistics (stats), how deep our history is for that query (coverage) and the individual sold items (items).
Use it from ChatGPT, Claude or Cursor (MCP)
Comps is also an MCP server, so an AI assistant can answer “what did this sell for on Vinted?” with real sold prices. Server URL: https://swanum.com/comps/mcp (Streamable HTTP). Without a key you get a few searches to try it. With your free key you get your account's monthly quota and the 7-country comparison: use https://swanum.com/comps/mcp?key=YOUR_KEY as the URL, or send the key as Authorization: Bearer <key>.
- ChatGPT: Settings → Apps & Connectors → Advanced → Developer mode, then create a connector with the URL above (no authentication), using the URL with your key.
- Claude: Settings → Connectors → Add custom connector, paste the URL with your key.
- Cursor / VS Code / other clients:
{"mcpServers": {"comps": {"url": "https://swanum.com/comps/mcp?key=YOUR_KEY"}}}
Step-by-step guide with an example: Comps in ChatGPT & Claude.
Two tools: vinted_sold_prices (sales count, median and range, time to sell, a few example sales) and vinted_compare_countries (the same search across the 7 countries, needs a key). Each answer links to the full result on this site.
Chrome extension
The Comps Chrome extension shows the same sold prices in a small box on every Vinted listing (all 7 countries): median, typical range, how fast similar items sold and where the listing's price sits. Without a key it gives a few free checks a day; paste your key in the extension's settings to use your account's monthly quota and check every listing automatically. Only the listing's title, brand and country are sent. Add to Chrome.
Authentication
Send your key in X-API-Key: <key>. Authorization: Bearer <key> works too. Keys start with sc_live_, are shown once, and we only store a hash — if you lose one, revoke it and create another in the dashboard.
GET /vinted/sold
Items that sold on Vinted, from our tracked history. Vinted has no sold search; we watch listings over time and record a sale only when the item page itself says “Sold”. A listing that disappears without that label (deleted or hidden) is never counted.
| Param | Type | Default | Description |
|---|---|---|---|
q | string, 2–200 | required | Words matched against item titles (all words must appear — accents, punctuation and a plural s don't matter: Dr. Martens finds “Dr Martens”, xt6 finds “XT-6”). See matching. |
country | FR DE UK IT ES NL PL | FR | Which Vinted site. Prices are in that site's currency (EUR, GBP, PLN). |
brand | string | — | Exact brand as Vinted shows it, case-insensitive. |
size | string | — | Exact size label, e.g. M, 42. |
condition | new · used · any | any | new = with or without tags; used = very good / good / satisfactory. |
days | 1–365 | 90 | Sales detected in the last N days. |
limit | 1–240 | 50 | Items returned (stats always use every match). |
Every query you send is added to our tracking list for that country, so coverage grows where people actually search.
{
"query": {"q": "nike air force 1", "country": "FR", ...},
"source": "vinted",
"cached": false,
"fetched_at": "2026-10-02T09:14:03+00:00",
"stats": {
"count": 41, "currency": "EUR",
"median": 38.0, "mean": 40.12, "p25": 30.0, "p75": 48.0,
"min": 15.0, "max": 85.0, "outliers_removed": 2,
"median_days_to_sell": 4.1,
"sell_through": [
{"within_days": 1, "listings": 180, "sold": 14, "sold_pct": 8,
"median_price_sold": 30.0, "median_price_unsold": 42.0},
{"within_days": 7, "listings": 122, "sold": 31, "sold_pct": 25,
"median_price_sold": 33.0, "median_price_unsold": 45.0}
]
},
"coverage": {
"tracked_listings": 612, "tracking_since": "2026-09-24T22:41:30+00:00",
"days_tracked": 8, "disappeared_unknown": 37,
"note": "Sold history is built by tracking listings over time; depth grows daily."
},
"items": [{
"id": "vinted:FR:10124550958",
"title": "Nike Air Force 1 blanches",
"price": 40.0, "currency": "EUR",
"condition": "Très bon état", "condition_normalized": "used",
"brand": "Nike", "size": "42",
"listed_at": "2026-09-25T07:48:00+00:00",
"last_active_at": "2026-09-26T08:03:12+00:00",
"sold_detected_at": "2026-09-28T08:05:40+00:00",
"days_to_sell": 3.0,
"sold_date_known": true,
"outlier": false,
"url": "https://www.vinted.fr/items/10124550958-...",
"image_url": "https://images1.vinted.net/..."
}]
}
priceis the last asking price we saw before the sale (Vinted doesn't publish the final negotiated amount).listed_atis when the listing went up, read off its listing number: Vinted numbers every listing in one rising sequence across all countries, and we log where that sequence stands every time we read a search page. Accurate to about an hour. A listing saved as a draft and published later looks older than it is.- The sale happened between
last_active_at(last seen for sale) andsold_detected_at(first seen sold).days_to_sellissold_detected_at − listed_at, so read it as “sold within”. sold_date_knownisfalsewhen we found the sale after it happened rather than watching it happen. Then the sale day is unknown,listed_atis the listing date to go by, and there is no time to sell (days_to_sellisnull). These sales are left out of the weekly trend and the “Just sold” feed.coverage.tracking_sinceis when we started tracking this search. Sales confirmed shortly before that can also appear, so some sold dates are earlier thantracking_since.outlieristruefor a sale that is left out ofstats(see How stats work).stats.countis the number after outliers are removed;itemsshows all sales.median_days_to_sellonly covers items that sold.sell_throughis the fuller picture: of the listings we watched from about the hour they went up, the share sold after 1, 3, 7, 14 and 30 days — a listing only counts at a horizon once we know its state there (a late check leaves it out rather than counting it as a slow sale).median_price_soldvsmedian_price_unsoldshows what the quick sellers were asking compared with the ones still waiting.
How matching works
Every response has a match object. "mode": "exact": every word of q is in the title. When the exact match has under 5 sales we also try a broad match and return it if it counts more: a category word may be the same thing in any of our languages (tracksuit also finds tuta, chándal, survêtement, dres…), and is dropped when a model number still pins the item down (New Balance 574 buty → “New Balance 574”). Brand and model words are never loosened, and neither are size words (small, mini, medium, large, petit, klein, PM, MM, GM, BB…) — a broad Air Max Plus never counts Air Max 90 sales. A broad answer says exactly what it matched in match.note, so you don't need to retry translated or shortened queries yourself.
GET /vinted/compare
One search across all 7 Vinted countries in one request: sale count, median and typical range per country, each median also in EUR (ECB reference rate, date in fx), and the gap between the cheapest and dearest country with 3+ sales. Same matching as /vinted/sold. Parameters: q, country (your home market, listed first), days (1–365, default 90). Counts as one request.
curl -G "https://swanum.com/comps/api/v1/vinted/compare" -H "X-API-Key: $COMPS_KEY" \
--data-urlencode "q=nike tech fleece" -d country=FR
→ {"full": false, "countries": [{"country": "FR", "count": 20, "currency": "EUR", "median": 24.98, "median_eur": 24.98, "locked": false, …},
{"country": "UK", "count": 115, "currency": "GBP", "median": 20.0, "median_eur": 23.87, "locked": false, …},
{"country": "PL", "count": 38, "median": null, "locked": true, …}, …],
"gap": {"cheapest": "UK", "dearest": "FR", "pct": 5, …}, "fx": {"base": "EUR", "date": "2026-10-22", …}}
Pro shows every country in full. On the free beta ("full": false) your country and the next one with the most sales are in full and the other five carry their sale count only ("locked": true); gap is then worked out over the two shown. Pro is planned at $29/month and isn't open yet.
Alerts: POST /alerts · GET /alerts · DELETE /alerts/{id}
History starts when a search is first tracked, so a new search is often thin. When an answer has under 5 sales it carries a notify object with the exact request to make:
curl -X POST "https://swanum.com/comps/api/v1/alerts" -H "X-API-Key: $COMPS_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "Dr. Martens 1460", "source": "vinted", "market": "ES"}'
→ 201 {"id": 42, "q": "dr. martens 1460", "source": "vinted", "market": "ES", "email": "you@example.com",
"status": "waiting", "sales_now": 1, "notify_at_sales": 5, "expires_at": "…", "notified_at": null}
We email your account once the search has 5 sales (checked hourly) — one email, then the alert is done; with no email after 30 days it expires quietly. Creating it also puts the search at the front of our tracking queue. Posting the same search again returns the same alert (200). GET /alerts lists yours with status (waiting · ready · notified · expired) and today's sales_now, so an app can poll it instead of waiting for the email. Up to 100 open alerts per account; free, never counted against your quota. market is the Vinted country (FR DE UK IT ES NL PL) or the eBay marketplace (US).
Watches: POST /watches · GET /watches · DELETE /watches/{id}
A watched Vinted search keeps going where an alert stops: we email your account at most once a day when the search has sales newer than the last email, with the median then and now (and flag a move of 10% or more). Free accounts can watch 1 search, Pro up to 50 — GET /watches returns your allowance. Watching is free, never counted against your quota.
curl -X POST "https://swanum.com/comps/api/v1/watches" -H "X-API-Key: $COMPS_KEY" \
-H "Content-Type: application/json" -d '{"q": "new balance 530", "country": "UK"}'
→ 201 {"id": 7, "q": "new balance 530", "country": "UK", "base_count": 40, "base_median": 25.0, "last_sale_at": "…", "last_mailed_at": null, …}
Posting the same search again returns the same watch (200); over your allowance it's 409 watch_limit.
GET /usage
{"plan": "beta", "quota": 1000, "used": 12, "remaining": 988,
"period_start": "2026-10-01T00:00:00+00:00", "resets_at": "2026-11-01T00:00:00+00:00"}
Free, doesn't count against your quota.
GET /health
No key needed. Per-source status from our monitor — the same data as the status page.
How stats work
- Outliers are removed first with Tukey's rule: anything below Q1 − 1.5×IQR or above Q3 + 1.5×IQR.
outliers_removedtells you how many. Then anything under a tenth of the median goes too (a $0.95 console). With fewer than 4 sales nothing is removed. - A search matches the model you typed, not its siblings:
iphone 13leaves outiPhone 13 Pro,Pro Maxandmini,nintendo switchleaves out the Lite, the OLED and the Switch 2 — add the word to the search to get them. Clones and empty boxes (Not a PS5,box only,replica) are left out. - Percentiles use linear interpolation (numpy's default), so you can reproduce every number from
items. countis the number of sales after outlier removal.itemslists every sale, including the removed ones — each carries"outlier": trueorfalse(same rule as above), so you can filter them out or show them.
Errors
Every error has the same shape:
{"error": {"code": "quota_exceeded", "message": "...", "docs": "https://swanum.com/comps/docs#error-quota_exceeded"}}
| Status | code | Meaning | Counted? |
|---|---|---|---|
| 400 | invalid_params | A parameter is missing or out of range; the message names it. | No |
| 400 | fresh_not_in_plan | fresh=true isn't included in your plan. | No |
| 400 | alerts_need_direct_key | Alerts email the account behind a direct key; marketplace (RapidAPI) calls have no account to email. | No |
| 401 | missing_api_key | No key in the request. | No |
| 401 | invalid_api_key | Unknown or revoked key. | No |
| 402 | quota_exceeded | Monthly quota used up. The message has the reset date and how to get more. | No |
| 404 | not_found | No such alert on your account. | No |
| 409 | too_many_alerts | 100 open alerts already; delete some or wait for them to fire. | No |
| 409 | watch_limit | Your account watches as many searches as it can (1 free, 50 on Pro); delete one to swap it. | No |
| 429 | rate_limited | Too many requests per second. Wait Retry-After seconds. | No |
| 503 | source_unavailable | The marketplace is blocking or down. Retry after Retry-After. | No |
| 500 | internal_error | Our bug. Quote the X-Request-Id header when you write to us. | No |
Rate limits & quota
Every API response carries X-RateLimit-Limit, X-RateLimit-Remaining (per second), X-Quota-Limit, X-Quota-Remaining (per period) and X-Request-Id. Free: 1 request/second; paid plans: 5. Quotas are on the pricing page.
Code samples
Runnable Python and Node examples: github.com/akor-pixel/vinted-sold-prices-api
Python
import os, requests
r = requests.get(
"https://swanum.com/comps/api/v1/vinted/sold",
headers={"X-API-Key": os.environ["COMPS_KEY"]},
params={"q": "levis 501", "country": "DE", "condition": "used"},
timeout=30,
)
r.raise_for_status()
data = r.json()
print(data["stats"]["median"], data["stats"]["currency"], "from", data["stats"]["count"], "sales")JavaScript (Node 18+ / browser server-side)
const url = new URL("https://swanum.com/comps/api/v1/vinted/sold");
url.search = new URLSearchParams({ q: "levis 501", country: "DE" });
const res = await fetch(url, { headers: { "X-API-Key": process.env.COMPS_KEY } });
if (!res.ok) throw new Error((await res.json()).error.message);
const { stats, items } = await res.json();
console.log(stats.median, stats.currency, items.length);PHP
$q = http_build_query(["q" => "levis 501", "country" => "DE"]);
$ch = curl_init("https://swanum.com/comps/api/v1/vinted/sold?$q");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("COMPS_KEY")],
]);
$data = json_decode(curl_exec($ch), true);
echo $data["stats"]["median"] . " " . $data["stats"]["currency"];curl
curl -G https://swanum.com/comps/api/v1/usage -H "X-API-Key: $COMPS_KEY"
FAQ
Does Vinted have an official API for sold prices?
No. Vinted has no public API, and its site does not let you search sold items. Comps builds that history by tracking listings over time and confirming each sale on the item page.
How fresh is the data?
It is a tracked history that deepens daily (responses are cached 15 minutes). Sales are detected continuously; each response has coverage telling you how long we have tracked that query, so you can tell “no sales” from “not tracked yet”.
Can Comps alert me to new Vinted listings?
Yes. The deal alert watches Vinted for new listings that match your filters (up to 5) and notifies you within about 30 seconds. It is free during the pilot and planned at €29/month afterwards.
Is there a no-code option?
Yes — the lookup page shows the same numbers in the browser.
Changelog
- 2026-11-08 — Size words (small, mini, medium, large, petit, klein, PM, MM, GM, BB…) are never dropped in a broad match, so “Ophidia Small” no longer counts other sizes. Sales removed as outliers are now flagged in the list (
outlier). - 2026-11-02 — Chrome extension: sold prices on every Vinted listing.
- 2026-10-31 — MCP server at
https://swanum.com/comps/mcpfor ChatGPT, Claude and Cursor (how to connect). - 2026-10-22 — eBay is no longer covered:
/ebay/soldreturns410 ebay_removedand isn't billed. Vinted (7 countries) is the whole product. - 2026-10-22 —
GET /vinted/compare(one search across the 7 Vinted countries; all 7 in full on Pro) and watched searches (POST/GET/DELETE /watches, daily email on new sales; 1 free, 50 on Pro). - 2026-10-11 — Alerts in the API (
POST/GET/DELETE /alerts); thin answers carry anotifyobject. Matching ignores accents, punctuation and plural s, and falls back to a labelled broad match (matchobject) when the exact one has under 5 sales. - 2026-10-07 —
/ebay/soldis live for eBay US (other eBay sites still return 503). Vinted searches with no history yet show current asking prices (clearly labelled) while tracking starts. - 2026-09-24 — Vinted sold tracking starts in FR, DE, UK, IT, ES, NL, PL. API v1 contract published;
/ebay/soldreturns 503 until eBay can be served without logging in.