Comps

Documentation

Base URL https://swanum.com/comps/api/v1 · JSON over HTTPS · openapi.json

Quickstart (under 2 minutes)

  1. Sign up with your email — no password, no card. Your first key is shown once on the dashboard.
  2. 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=FR

You 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.

ParamTypeDefaultDescription
qstring, 2–200requiredWords matched against item titles (all words must appear).
countryFR DE UK IT ES NL PLFRWhich Vinted site. Prices are in that site's currency (EUR, GBP, PLN).
brandstring—Exact brand as Vinted shows it, case-insensitive.
sizestring—Exact size label, e.g. M, 42.
conditionnew · used · anyanynew = with or without tags; used = very good / good / satisfactory.
days1–36590Sales detected in the last N days.
limit1–24050Items 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/..."
  }]
}
  • price is the last asking price we saw before the sale (Vinted doesn't publish the final negotiated amount).
  • listed_at is 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) and sold_detected_at (first seen sold). days_to_sell is sold_detected_at − listed_at, so read it as “sold within”.
  • median_days_to_sell only covers items that sold. sell_through is 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_sold vs median_price_unsold shows what the quick sellers were asking compared with the ones still waiting.

GET /ebay/sold

Live for eBay US. eBay puts its sold search behind a sign-in, so we don't use it: we find listings in eBay's public search while they're live and read each public item page after it ends. History starts on the day we began following a search (US since September 2026) and deepens daily. Other marketplaces return 503 source_unavailable (not counted) until they're tracked.
ParamTypeDefaultDescription
qstring, 2–200requiredSearch text.
marketplaceUS UK DE CA AU FR IT ESUSebay.com, .co.uk, .de, .ca, .com.au, .fr, .it, .es
conditionnew · used · refurbished · for_parts · anyany
min_price, max_pricenumber—In the marketplace currency.
days1–9090eBay exposes about 90 days of sold history.
category_idinteger—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.
excludecomma-separated—Drop items whose title contains any of these words, e.g. box,case,lot.
limit1–24050
sortdate_desc · price_asc · price_descdate_desc
include_best_offerboolfalseInclude accepted-Best-Offer sales in stats (their real price is hidden by eBay).
freshboolfalseBypass 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_removed tells 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.
  • count is 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"}}
StatuscodeMeaningCounted?
400invalid_paramsA parameter is missing or out of range; the message names it.No
400fresh_not_in_planfresh=true isn't included in your plan.No
401missing_api_keyNo key in the request.No
401invalid_api_keyUnknown or revoked key.No
402quota_exceededMonthly quota used up. The message has the reset date and how to get more.No
429rate_limitedToo many requests per second. Wait Retry-After seconds.No
503source_unavailableThe marketplace is blocking or down. Retry after Retry-After.No
500internal_errorOur 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/sold returns 503 until eBay can be served without logging in.