Quickstart
- Get a key. Enter your email on the home page. Free keys include 100 lookups a month, no card needed.
- Make a request. Send your search as
qand your key in theX-API-Keyheader. - Use the numbers.
stats.medianis the typical sold price;itemslists every sale behind it.
curl "https://soldstack.dev/v1/sold?q=coach+tabby" \
-H "X-API-Key: ss_your_key"
Base URL: https://soldstack.dev. Everything is HTTPS and JSON. Uptime and incidents: status page.
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.
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:
poshmark(US) orposhmark_ca(Poshmark Canada). Canadian results are in CAD (stats.currencyand each item'scurrencysay so), link to poshmark.ca, andmedian_net_payoutuses Poshmark Canada's fee: a flat C$3.95 under C$20, otherwise 20%, before GST/HST on the fee. eBay, Mercari, Depop, Grailed, StockX and Vinted are planned. - sourcestring · default poshmark
- Which marketplace to search:
poshmarkormercari(beta). eBay, Depop, Grailed, StockX and Vinted are planned.Mercari (beta). Sold prices from Mercari's public sold search: condition, brand and size included. Mercari doesn't show when an item sold, so for
source=mercari:sold_at,days_to_sellandstats.median_days_to_sellare alwaysnull.dayscan't narrow the results: undated sales are kept, not dropped, andcleaning.undatedsays how many there are.- Results can include older sales, because Mercari ranks sold items by relevance, not date.
- Prices are the last listed price; accepted offers may be a little lower.
median_net_payoutuses Mercari's seller fee: a flat 10% of the sale price.
SoldStack isn't affiliated with Mercari.
- limitinteger · 1–200 · default 50
- How many sold listings to return in
items. Stats always use every clean match, whatever the limit. A normal lookup reads the 48 most recent sales, so you'll get at most 48 back unless you passdeep=true. - deepboolean · default false
- Read up to about 190 recent sales (four Poshmark result pages) instead of 48. Slower when not cached (about 10 seconds) and still counts as one lookup. Cached separately from the normal lookup. Up to 120 new deep lookups per 10 minutes per key (cached repeats don't count); past that you get a 429 saying so.
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,mediumandSize Mediummatch each other, and8matches8.0. - conditioncomma-separated
new,used, or any exact condition value, e.g.used_like_new,used_excellent. Plain words work too:nwt,like new,excellent,very good,good,fair,poor. Listings with no condition are left out when you filter.- daysinteger · 1–365
- Only sales from the last N days. Filters the sales behind
statstoo, unlikesort, which only orders the sales returned. To get the most recent sales, usedaysto set the window andsort=recentto order them. - 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. - exclude_sellercomma-separated
- Drop sales by these seller usernames (case-insensitive), e.g.
exclude_seller=closet_a,Closet_B, before the stats are computed. Counted incleaning.removedasexcluded_seller. Sales cached before sellers were recorded have nosellerand are never excluded. - departmentcomma-separated
- Only sales in these Poshmark departments (case-insensitive):
Women,Men,Kids,Home,Pets,Electronics. An unknown department returns a 400 listing the valid ones.department=kidsalso turns off the defaultkids_sizesremoval, so kids' items are kept. Sales cached before departments were recorded have nodepartment:kidsfalls back to the kids' size and title check,womenandmenkeep them, and other departments drop them. Counted inremovedasdepartment. - categorycomma-separated
- Only sales in these Poshmark categories (case-insensitive), e.g.
category=Jeans,Shorts. Sales with no recorded category are kept. Counted inremovedascategory. - sortrecent · price_asc · price_desc
- Order of the sales returned in
items:recentis newest first (sales with no sold date last),price_ascandprice_descby price. It only orders the returned list, sostatsnever change;limitthen keeps the first N in that order. Leave it out for the marketplace's order. To restrict which sales are used, seedays. - include_removedboolean · default false
- Add
removed_itemsto the response: the sales cleaning and your filters dropped, each with aremoved_reason(see removed_items[]), up tolimit. - cleanboolean · default true
- Clean comps (below). Set
clean=falseto 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 6means size 6. - replicas
- Listings whose title says replica, rep, dupe, fake, inspired, 1:1, look-alike or similar, unless you searched for those words. Price outliers (next) also catch most unlabeled fakes, which usually sell far below the real price.
- 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 asks for them. To price kids' clothing, put girls, boys, kids, toddler, youth or GS in
q:girls jeanskeeps the girls' sales. - 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.
- variants
- Listings of a different model of the same item, e.g. a Pillow or Twisted Tabby when you searched Tabby 26, are removed.
- duplicates
- The same listing showing up twice (for example on two result pages) counts once. Two separate listings of the same item are two sales.
- price_outliers
- Prices far outside the rest, once there are at least 8 sales to compare.
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:
{
"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
},
"by_condition": [
{ "condition": "new_with_tags", "label": "New with tags", "count": 12, "median": 228, "p10": 195, "p90": 265 },
{ "condition": "used_excellent", "label": "Excellent", "count": 2, "median": 170 }
],
"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",
"seller": "closet_name",
"department": "Women",
"category": "Bags"
}
],
"usage": { "used": 12, "limit": 100, "remaining": 88, "plan": "free", "resets": "2026-11-01" }
}
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% abovep90. - 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.
by_condition
The same sales split by the condition the seller listed, best condition first, so you can price the item you actually have. Conditions with no sales are left out, as are older listings that have no condition.
- conditionstring
new_with_tags,not_new_with_tags,used_like_new,used_excellent,used_goodorused_fair. The same values theconditionfilter takes.- labelstring
- Display name, e.g. "Like new".
- countinteger
- Sales in this condition.
- mediannumber · USD
- Middle sold price in this condition.
- p10 / p90number · USD · optional
- Typical range. Only included with 3 or more sales, since a range from one or two sales would mislead.
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 asstats.count). - removedobject
- Count per reason:
filters,excluded_seller,department,category,off_topic,replicas,bundles,kids_sizes,other_item_types,variants(a different model of the same item, e.g. a Pillow or Twisted Tabby when you searched Tabby 26),duplicates,price_outliers. Reasons with nothing removed are left out.include_removed=truereturns the sales behind these counts. - item_typestring · or null
- The kind of item the comps are about, e.g.
bags,pants/leggings,shoes.mixedwhen 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.
- notestring · optional
- Only present with
partial_matches(below): says those sales are not part of the stats.
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,MorOS(one size). - conditionstring · or null
- One of
new_with_tags,not_new_with_tags,used_like_new,used_excellent,used_good,used_fair.nullwhen 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.
- sellerstring · or null
- The seller's username (Poshmark). Missing on sales cached before this field was added. Use it with
exclude_seller. - departmentstring · or null
- Poshmark's department:
Women,Men,Kids,Home,PetsorElectronics. Missing on older cached sales. Use it with thedepartmentfilter. - categorystring · or null
- Poshmark's category, e.g.
JeansorBags. Missing on older cached sales. Use it with thecategoryfilter.
removed_items[]
Only with include_removed=true. The sales that were dropped before the stats, in the marketplace's order, up to limit. Each has every field of items[] plus:
- removed_reasonstring
- Why it was dropped: one of the
cleaning.removedkeys (filters,excluded_seller,department,category,off_topic,replicas,bundles,kids_sizes,other_item_types,variants,duplicates,price_outliers). Counting them all givescleaning.removed.
partial_matches[]
Only when cleaning leaves no sales and at least one sale was removed as off_topic. Up to 10 of those off-topic sales, closest first, so you can show "nearest matches" instead of nothing. They are not used in stats, and cleaning.note says so. Each has every field of items[] plus:
- match_scorenumber · 0.5–1
- The share of your search's meaningful words found in the title and brand, rounded to 2 decimals. Sales under 0.5 are left out, so the list can be empty.
usage
On every /v1/sold and /v1/verdict response, counted after this request. The same numbers come back as headers: X-Lookups-Used, X-Lookups-Limit, X-Lookups-Remaining and X-Plan.
- usedinteger
- Lookups used this month. A cached repeat doesn't add to it.
- limitinteger
- Your plan's lookups per month.
- remaininginteger
limitminusused, never below 0. At 0 the next fresh lookup gets a 429.- planstring
free,starter,growthorscale.- resetsstring · ISO date
- The first day of next month (UTC), when the count goes back to 0.
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.
curl "https://soldstack.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
compsas evidence. - filtersoptional
- Every filter from
/v1/soldworks here too:size,condition,days,min_price,max_price,exclude,exclude_seller,department,category,sort,include_removed,clean.usageand theX-Lookups-*headers come back too.
How the verdict is decided
- pass
- After the marketplace fee, the expected profit (
median_net_payout − cost) is belowmin_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
reasonsays which.
Response
{
"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.nullwhen there were no comps. - roinumber · or null
- Profit divided by cost:
3.16means 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 intostats.
MCP server
SoldStack is also a remote MCP server, so an AI agent (Claude, Cursor, ChatGPT and others) can price resale items itself. There's nothing to install: point your client at the URL and send your API key. Each tool call is one lookup, metered exactly like the REST API.
{
"mcpServers": {
"soldstack": {
"type": "http",
"url": "https://soldstack.dev/mcp",
"headers": { "X-API-Key": "ss_your_key" }
}
}
}
Add it to Claude (claude.ai and the Claude Desktop app)
Custom connectors sync across your Claude account, so you add SoldStack once and it shows up on the web and in the desktop app. They're available on the Free, Pro, Max, Team and Enterprise plans (Free allows one custom connector; on Team and Enterprise an owner adds it first, then members connect).
- Get your key: a free one takes a few seconds, or sign in on your account page.
- In claude.ai (or the Claude Desktop app), open Customize, then Connectors. Click + Add, then Add custom connector. On Team and Enterprise an owner goes to Organization settings > Connectors > Add > Custom instead.
- Name it
SoldStackand enter the server URLhttps://soldstack.dev/mcp. Continue. - SoldStack uses an API key, not a sign-in, so set authentication to No sign in. Then under Request headers add the header name
X-API-Keywith your key (ss_...) as the value. Click Add. (Authorization: Bearer ss_your_keyworks too.) - Start a chat, open the connectors menu and switch SoldStack on. Ask "What did a Lululemon Align legging sell for on Poshmark?" or "I can buy this Coach Tabby 26 for $40, buy or pass?"
Menu names move around a little between Claude releases. If yours differs, look for Connectors and Add custom connector, and see Anthropic's custom connector guide. If your version has no field for request headers, use the URL form https://soldstack.dev/mcp?api_key=ss_your_key as the server URL. That URL contains your key, so keep it private like a password and don't paste it into shared chats or screenshots.
Claude Code, Cursor and other clients
Use the JSON above. For Claude Code you can run this instead:
claude mcp add --transport http soldstack https://soldstack.dev/mcp --header "X-API-Key: ss_your_key"
If your client only takes a URL, use https://soldstack.dev/mcp?api_key=ss_your_key and keep that URL private.
For AI agents: call the tools one item at a time, not in parallel. If a call returns busy, wait the number of seconds it says and call again.
Tools
- sold_compsquery, size, condition, days, limit, deep, exclude_seller, sort, department, category, include_removed
- What an item sold for: the same
stats,by_conditionandcleaningas /v1/sold, plus the matching sales,removed_itemsandpartial_matcheswhen they apply, and the sameusageobject. - buy_or_passquery, cost, min_profit, size, condition
- BUY, MAYBE or PASS for an item at a given cost, with the reason, like /v1/verdict, plus
usage.
Errors
Errors use normal HTTP status codes and a JSON body with a plain-English detail, e.g. {"detail": "invalid API key"}.
- 400Bad request
qis shorter than 2 or longer than 100 characters,sourceisn't live yet,conditionhas an unknown value, ordepartmentisn't one of Women, Men, Kids, Home, Pets, Electronics. Nothing is counted.- 401Unauthorized
- The API key is missing or wrong.
- 422Invalid parameter
qis missing, or a parameter has the wrong type or range, e.g.limit=abcorlimit=500.detailis a readable string such as"limit: Input should be less than or equal to 200"; the structured list is inerrors.- 429Limit reached
- Either you've used this month's lookups (upgrade on your account page, or wait until the 1st), or you're sending more than 120 requests per minute per key, or more than 120 new
deep=truelookups per 10 minutes. Thedetailsays which. Slow down and retry in a minute. - 503Busy
- A lot of fresh searches are queued ahead of yours. The
Retry-Afterheader and thedetailsay how many seconds to wait; retry after that. If the line was simply full, nothing was counted. If your request waited its turn and timed out, SoldStack keeps fetching it in the background (it counts as one lookup) so the retry returns instantly from cache and isn't counted again. Send one request at a time to avoid it. - 502Marketplace error
- The marketplace didn't answer properly or in time (e.g. "Poshmark didn't respond in time. Try again in a minute."). Nothing was counted against your plan. Retry after a minute.
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/soldor/v1/verdictthat has to fetch fresh data, whatever thelimitor 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.
Code examples
No code? Use your key in Google Sheets with =SOLDPRICE("coach tabby 26").
JavaScript (Node 18+)
const res = await fetch(
"https://soldstack.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
import os, requests
r = requests.get(
"https://soldstack.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")
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
medianrather 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.
- How accurate it is: what cleaning removed in real searches, how confidence is decided and what it doesn't catch are on How accurate is this?
- Mercari (beta): Mercari doesn't show when an item sold, so
source=mercariresults have no sold dates or days to sell,daysdoesn't filter them, and they can include older sales. SoldStack isn't affiliated with Mercari.
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.