Documentation
Base URL https://swanum.com/comps/api/v1 · JSON over HTTPS · openapi.json
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).
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). |
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,
"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”. 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.
GET /ebay/sold
503 source_unavailable (not counted) until they're tracked.| Param | Type | Default | Description |
|---|---|---|---|
q | string, 2–200 | required | Search text. |
marketplace | US UK DE CA AU FR IT ES | US | ebay.com, .co.uk, .de, .ca, .com.au, .fr, .it, .es |
condition | new · used · refurbished · for_parts · any | any | |
min_price, max_price | number | — | In the marketplace currency. |
days | 1–90 | 90 | eBay exposes about 90 days of sold history. |
category_id | integer | — | eBay leaf category, e.g. 139971 (Video Game Consoles) — keeps a console apart from its controllers and cover plates. Sales read since Sep 28, 2026 carry category_id, category and item_specifics (Model, Storage Capacity, UPC…); older ones have null and don't match this filter. |
exclude | comma-separated | — | Drop items whose title contains any of these words, e.g. box,case,lot. |
limit | 1–240 | 50 | |
sort | date_desc · price_asc · price_desc | date_desc | |
include_best_offer | bool | false | Include accepted-Best-Offer sales in stats (their real price is hidden by eBay). |
fresh | bool | false | Bypass the 15-minute cache (included in the free beta); still one request. |
GET /usage
{"plan": "beta", "quota": 10000, "used": 12, "remaining": 9988,
"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. With fewer than 4 sales nothing is removed. - Percentiles use linear interpolation (numpy's default), so you can reproduce every number from
items. countis the number of sales after outlier removal.
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 |
| 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 |
| 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
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"
Changelog
- 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.