API docs.

Send a search, get back the sales that actually match, the numbers you'd otherwise work out yourself, and a buy or pass verdict.

Start here

Quickstart

  1. Get a key. Enter your email on the home page. Free keys include 100 lookups a month, no card needed.
  2. Make a request. Send your search as q and your key in the X-API-Key header.
  3. Use the numbers. stats.median is the typical sold price; items lists every sale behind it.
Terminal
curl "https://soldstack.fly.dev/v1/sold?q=coach+tabby" \
  -H "X-API-Key: ss_your_key"

Base URL: https://soldstack.fly.dev. Everything is HTTPS and JSON.

Keys

Authentication

Every request needs your API key. Send it in the X-API-Key header (recommended), or as an api_key query parameter if your tool can't set headers.

Keep your key on your server. Don't put it in browser JavaScript or a mobile app, where anyone can read it. Call SoldStack from your backend and pass the results to your app.

You can see your key, plan and usage on your account page.

Endpoint

GET /v1/sold

Returns recently sold listings that match a search, with summary stats.

Query parameters

qstring · required
What to search for, 2–100 characters. Brand plus item works best, e.g. lululemon align 6. Case and extra spaces are ignored.
sourcestring · default poshmark
Which marketplace to search. Only poshmark is live today; eBay, Mercari, Depop, Grailed, StockX and Vinted are planned.
limitinteger · 1–200 · default 50
How many sold listings to return in items. Stats always use every clean match, whatever the limit.

Filters

Optional. Filters work on the cached search, so trying different filters on the same search doesn't use extra lookups.

sizestring
Only this size. Letter sizes are normalized, so M, medium and Size Medium match each other, and 8 matches 8.0.
conditioncomma-separated
new, used, or any exact condition value, e.g. used_like_new,used_excellent. Listings with no condition are left out when you filter.
daysinteger · 1–365
Only sales from the last N days.
min_price / max_pricenumber
Only sales in this price range.
excludecomma-separated
Drop listings whose title contains any of these words, e.g. exclude=stained,damaged.
cleanboolean · default true
Clean comps (below). Set clean=false to get the marketplace's raw results; your filters still apply.

Clean comps

Marketplace search is loose: "lululemon align" also returns joggers, bodysuits and bundles, and "nike dunk low" mixes toddler sizes with adult pairs. Before computing anything, SoldStack removes:

off_topic
Titles missing one of your search words. Numbers in your search can also match the size, so lululemon align 6 means size 6.
bundles
Bundles, lots and "X & Y" listings of two different items, unless you searched for a bundle.
kids_sizes
Toddler, youth and GS sizes, unless your search says kids, toddler, youth or GS.
other_item_types
When most matches are one kind of item (bags, leggings, sneakers), other kinds are dropped. Name the type in your search to choose it yourself.
duplicates
Identical listings that sold the same day count once.
price_outliers
Prices far outside the rest, once there are at least 8 sales to compare.

What comes back

Response

A JSON object with the normalized query, the source, a stats summary, a cleaning report and the items that sold. This is a real response, trimmed to one item:

200 OK · application/json
{
  "query": "coach tabby",
  "source": "poshmark",
  "stats": {
    "count": 48,
    "median": 194.0,
    "mean": 247.25,
    "p10": 107.0,
    "p90": 409.9,
    "min": 50.0,
    "max": 775.0,
    "median_days_to_sell": 4.2,
    "median_net_payout": 155.2,
    "suggested_max_buy": 51.73
  },
  "cleaning": {
    "found": 48,
    "used": 46,
    "removed": { "bundles": 1, "other_item_types": 1 },
    "item_type": "bags",
    "confidence": "medium",
    "confidence_note": "46 matching sales, but prices vary. Add a model, size or condition to narrow it."
  },
  "items": [
    {
      "source": "poshmark",
      "id": "6aa7b1f5353f0a1005ca7d66",
      "title": "Coach Tabby 20",
      "brand": "Coach",
      "size": "OS",
      "condition": "used_good",
      "price": 135.0,
      "currency": "USD",
      "sold_at": "2026-09-22T10:58:58-07:00",
      "listed_at": "2026-09-14T01:51:24-07:00",
      "days_to_sell": 8.4,
      "url": "https://poshmark.com/listing/Coach-Tabby-20-6aa7b1f5353f0a1005ca7d66",
      "image": "https://di2ponv0v5otw.cloudfront.net/posts/…jpeg"
    }
  ]
}

stats

Computed over every clean match, not just the ones returned in items. When nothing matched, stats is just {"count": 0} and items is empty.

countinteger
How many sold listings matched.
mediannumber · USD
The middle sold price. The best single "what it sells for" number, because one unusually high sale doesn't move it.
meannumber · USD
The average sold price.
p10 / p90number · USD
The realistic low and high end: 10% of sales were below p10, 10% above p90.
min / maxnumber · USD
Cheapest and most expensive sale.
median_days_to_sellnumber · days · or null
Typical time from listing to sale. A low number means the price moves quickly.
median_net_payoutnumber · USD
What the seller keeps from the median price after the marketplace fee. On Poshmark: $2.95 flat under $15, otherwise 20%.
suggested_max_buynumber · USD
A third of the net payout: a common reseller rule for the most to pay when sourcing an item to flip.

cleaning

What was removed and how far to trust the stats.

foundinteger
Sold listings the marketplace returned.
usedinteger
How many were kept and used for stats (same as stats.count).
removedobject
Count per reason: filters, off_topic, bundles, kids_sizes, other_item_types, duplicates, price_outliers. Reasons with nothing removed are left out.
item_typestring · or null
The kind of item the comps are about, e.g. bags, pants/leggings, shoes. mixed when no single kind dominates.
confidencehigh · medium · low
high: 15+ matching sales with consistent prices. medium: 6+ sales, or prices that vary. low: few sales, very different prices, or no matches.
confidence_notestring
A plain-English reason and a tip, ready to show your users.

items[]

Every sold listing comes back in the same shape, whatever the marketplace. Items are in the marketplace's order, which is by relevance, not date.

sourcestring
Marketplace the sale came from.
idstring
The marketplace's listing ID.
titlestring
Listing title as the seller wrote it.
brandstring · or null
Brand, when the seller set one.
sizestring · or null
Size as listed, e.g. 6, M or OS (one size).
conditionstring · or null
One of new_with_tags, not_new_with_tags, used_like_new, used_excellent, used_good, used_fair. null when the seller didn't say.
pricenumber
The price it sold at, in currency.
currencystring
ISO currency code, usually USD.
sold_atstring · ISO 8601 · or null
When it sold.
listed_atstring · ISO 8601 · or null
When it was first listed.
days_to_sellnumber · or null
Days between listing and sale.
urlstring
Link to the original listing.
imagestring · or null
Link to the listing's main photo.

Endpoint

GET /v1/verdict

Should you buy it? Send the item and what you'd pay (say, the thrift-store tag price) and get buy, pass or maybe, with a reason you can show your users. It uses the same clean comps and filters as /v1/sold and costs the same single lookup, and asking again for the same item is free for 6 hours.

Terminal
curl "https://soldstack.fly.dev/v1/verdict?q=nike+dunk+low&cost=15&days=90" \
  -H "X-API-Key: ss_your_key"

Query parameters

qstring · required
The item, as specific as you can, e.g. coach tabby 26.
costnumber · required
What you'd pay for it.
min_profitnumber · default 10
The smallest profit after fees that's worth the effort.
limitinteger · 0–50 · default 5
How many of the comps to include in comps as evidence.
filtersoptional
Every filter from /v1/sold works here too: size, condition, days, min_price, max_price, exclude, clean.

How the verdict is decided

pass
After the marketplace fee, the expected profit (median_net_payout − cost) is below min_profit, or it's a loss.
buy
The profit clears min_profit, the cost is at or under the max-buy price (a third of the net payout), confidence isn't low, prices agree (the p10–p90 range is no more than 1.5× the median), and it typically sells within 90 days. A wide price range usually means the search mixes different versions of the item, such as kids' and adult sizes or worn and deadstock pairs, so the verdict asks you to narrow it first.
maybe
Profitable on paper, but one of the buy checks fails. The reason says which.

Response

200 OK · application/json · trimmed
{
  "query": "nike dunk low",
  "verdict": "maybe",
  "reason": "Sells for about $78 in about 40 days; at $15 you'd keep about $47.40 after fees. But prices for this search range from $32 to $185, so add the model, size or condition to be sure you're comparing the same item.",
  "cost": 15,
  "expected_sale_price": 78.0,
  "expected_net_payout": 62.4,
  "expected_profit": 47.4,
  "roi": 3.16,
  "max_buy": 20.8,
  "days_to_sell": 40.0,
  "confidence": "medium",
  "stats": { … same as /v1/sold … },
  "cleaning": { … same as /v1/sold … },
  "comps": [ … up to limit sold items … ]
}
verdictbuy · pass · maybe
The answer.
reasonstring
One or two plain-English sentences explaining it.
expected_profitnumber · or null
Net payout after fees minus cost. Shipping is paid by the buyer on Poshmark. null when there were no comps.
roinumber · or null
Profit divided by cost: 3.16 means you'd make 3.16 times what you paid.
max_buy / days_to_sell / confidence
Same meaning as in /v1/sold, repeated here so you don't have to dig into stats.

When something's wrong

Errors

Errors use normal HTTP status codes and a JSON body with a plain-English detail, e.g. {"detail": "invalid API key"}.

400Bad request
q is shorter than 2 or longer than 100 characters, source isn't live yet, or condition has an unknown value. Nothing is counted.
401Unauthorized
The API key is missing or wrong.
422Invalid parameter
q is missing, or a parameter has the wrong type or range, e.g. limit=abc or limit=500.
429Limit reached
You've used this month's lookups. Upgrade on your account page, or wait until the 1st.
502Marketplace error
The marketplace didn't answer properly. Nothing was counted against your plan. Retry after a minute.

Plans

Limits & caching

Free$0
100 lookups a month.
Starter$9 / month
2,500 lookups a month.
Growth$29 / month
15,000 lookups a month.
Scale$79 / month
60,000 lookups a month.
  • One lookup = one request to /v1/sold or /v1/verdict that has to fetch fresh data, whatever the limit or filters.
  • Repeats are free. Results are cached for 6 hours per search and source. Asking the same search again within 6 hours returns the cached result and doesn't count.
  • Counts reset on the 1st of each month (UTC).
  • Speed: cached results return in well under a second; fresh searches usually take 1–2 seconds. Requests to the marketplace are spaced out, so a burst of new searches can take longer.

Copy and paste

Code examples

JavaScript (Node 18+)

server.js
const res = await fetch(
  "https://soldstack.fly.dev/v1/sold?q=" + encodeURIComponent("coach tabby"),
  { headers: { "X-API-Key": process.env.SOLDSTACK_KEY } }
);
if (!res.ok) throw new Error((await res.json()).detail);
const { stats, items } = await res.json();

console.log(`Sells for about $${stats.median}, pay under $${stats.suggested_max_buy}`);

Python

prices.py
import os, requests

r = requests.get(
    "https://soldstack.fly.dev/v1/sold",
    params={"q": "coach tabby", "limit": 10},
    headers={"X-API-Key": os.environ["SOLDSTACK_KEY"]},
    timeout=30,
)
r.raise_for_status()
stats = r.json()["stats"]
print(f"Sells for about ${stats['median']} in {stats['median_days_to_sell']} days")

Good to know

About the data

  • Where it comes from: the marketplace's public sold listings, the same results a shopper sees when they filter a search to sold items.
  • Sold price: Poshmark shows the last listed price on a sold item. If the buyer and seller agreed a lower offer privately, the real final price may be a little lower. Use median rather than any single sale.
  • Matching: clean comps remove most mismatches, but a model name can still cover several versions (a Coach Tabby comes in sizes 20, 26 and 36 at very different prices). Add the model number, size or item type to your search for the tightest numbers, and check cleaning.confidence.
  • How old the sales are: the marketplace ranks sold results by relevance, so some sales can be months or years old. For today's prices, add days=90 (or your own window).
  • Freshness: fetched live on the first request, then cached for 6 hours.

Help

Support

Questions, a lost key, or a marketplace you need? Email aarontian2010@gmail.com. You'll get a reply from a person, usually within a day.

Using the API means you agree to the Terms. How we handle data is in the Privacy Policy. For code generators there's an OpenAPI spec.