<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sam Smith</title>
    <description>The latest articles on DEV Community by Sam Smith (@sam_compsapi).</description>
    <link>https://dev.to/sam_compsapi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4174389%2F4b17ffaf-14dc-435e-867a-96a8e1d91d08.png</url>
      <title>DEV Community: Sam Smith</title>
      <link>https://dev.to/sam_compsapi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sam_compsapi"/>
    <language>en</language>
    <item>
      <title>Your eBay sold-price data is 14.9% too high and the response looks fine</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:42:41 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/your-ebay-sold-price-data-is-149-too-high-and-the-response-looks-fine-2b92</link>
      <guid>https://dev.to/sam_compsapi/your-ebay-sold-price-data-is-149-too-high-and-the-response-looks-fine-2b92</guid>
      <description>&lt;p&gt;If your code reads eBay sold prices, it has a bug you cannot see. Not a parsing bug. The number comes back well formed, plausible, in the right currency, and wrong.&lt;/p&gt;

&lt;p&gt;I run CompsAPI, so I hold both the asking price and the accepted price on the same sale. That let me measure the gap instead of guessing at it. Here is what came out, and why no amount of better scraping fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;When an eBay listing sells through an accepted Best Offer, eBay's public sold page shows &lt;strong&gt;the asking price&lt;/strong&gt;. The amount the buyer actually paid is not on that page. There is no field holding it, no query parameter that reveals it, and no header that hints the row is affected.&lt;/p&gt;

&lt;p&gt;So the sale closed at $15,500, the page says $24,995, and your parser returns $24,995 with no error. Every tool reading that page returns the same wrong number, which is worse than disagreement, because when two sources agree you stop checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Over the 90 days to 10 October 2026: &lt;strong&gt;5,859 completed Best Offer sales across 5,840 distinct listings&lt;/strong&gt;, from 93 searches in 12 categories. Every pair is matched on listing id, so the asking price and the accepted price belong to the same item on the same sale. No row was discarded.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;99.0% read high&lt;/strong&gt; on the public sold page. 0.7% match within half a percent, 0.4% read low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median overstatement 14.9%.&lt;/strong&gt; A typical Best Offer sale shows about a seventh above what it closed at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p90 40.8%.&lt;/strong&gt; One sale in ten is overstated by more than two fifths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money weighted 14.2%&lt;/strong&gt;, across every dollar that changed hands, so this is not an artifact of cheap items.&lt;/li&gt;
&lt;li&gt;Median accepted price &lt;strong&gt;$240&lt;/strong&gt;, against a median asking price of &lt;strong&gt;$284&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How the tail behaves, which matters more than the median if you are setting a ceiling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Overstated by more than&lt;/th&gt;
&lt;th&gt;Share of accepted Best Offer sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;89.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;68.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;23.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  By category
&lt;/h2&gt;

&lt;p&gt;Every one of the 12 categories sits above 11% money weighted. The ones where sellers list high and expect to negotiate are the worst.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Sales matched&lt;/th&gt;
&lt;th&gt;Read high&lt;/th&gt;
&lt;th&gt;Median gap&lt;/th&gt;
&lt;th&gt;p90&lt;/th&gt;
&lt;th&gt;Money weighted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto parts&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;96.9%&lt;/td&gt;
&lt;td&gt;12.2%&lt;/td&gt;
&lt;td&gt;51.3%&lt;/td&gt;
&lt;td&gt;23.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sneakers&lt;/td&gt;
&lt;td&gt;857&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;td&gt;22.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graded trading cards&lt;/td&gt;
&lt;td&gt;390&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;td&gt;15.7%&lt;/td&gt;
&lt;td&gt;40.0%&lt;/td&gt;
&lt;td&gt;17.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jewellery&lt;/td&gt;
&lt;td&gt;652&lt;/td&gt;
&lt;td&gt;99.1%&lt;/td&gt;
&lt;td&gt;17.6%&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;td&gt;16.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Designer handbags&lt;/td&gt;
&lt;td&gt;723&lt;/td&gt;
&lt;td&gt;98.9%&lt;/td&gt;
&lt;td&gt;17.5%&lt;/td&gt;
&lt;td&gt;46.7%&lt;/td&gt;
&lt;td&gt;15.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video games and consoles&lt;/td&gt;
&lt;td&gt;446&lt;/td&gt;
&lt;td&gt;99.8%&lt;/td&gt;
&lt;td&gt;14.7%&lt;/td&gt;
&lt;td&gt;36.4%&lt;/td&gt;
&lt;td&gt;15.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phones and tablets&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;td&gt;35.9%&lt;/td&gt;
&lt;td&gt;15.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golf clubs&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;td&gt;13.7%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;14.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Musical instruments&lt;/td&gt;
&lt;td&gt;365&lt;/td&gt;
&lt;td&gt;98.4%&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coins and bullion&lt;/td&gt;
&lt;td&gt;362&lt;/td&gt;
&lt;td&gt;98.1%&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Luxury watches&lt;/td&gt;
&lt;td&gt;655&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;11.2%&lt;/td&gt;
&lt;td&gt;31.2%&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cameras and lenses&lt;/td&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;td&gt;97.9%&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;11.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why a better scraper does not help
&lt;/h2&gt;

&lt;p&gt;A Best Offer is a private negotiation. eBay publishes that the item sold and what it was listed at, and keeps the accepted amount off the public record. This is not a rendering quirk or a lazy loaded element. The number is not in the page, so there is nothing to select.&lt;/p&gt;

&lt;p&gt;That is the part worth internalising if you maintain a pricing pipeline: this is not a coverage problem you can close with more requests. It is a field that does not exist on the surface you are reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it does not average out
&lt;/h2&gt;

&lt;p&gt;The instinct with noisy data is that errors cancel. These do not. A Best Offer is almost never accepted &lt;strong&gt;above&lt;/strong&gt; the asking price, so the error is one sided and every affected comp pushes your estimate the same direction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The quiet version of this bug in a repricing loop.
&lt;/span&gt;&lt;span class="n"&gt;comps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_sold_comps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# some read the public sold page
&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;comps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;list_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;               &lt;span class="c1"&gt;# "just under market"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a third of those comps were accepted offers reading a median 14.9% high, &lt;code&gt;target&lt;/code&gt; is not the market, it is above it, and &lt;code&gt;0.97&lt;/code&gt; does not save you. You sit high, the item does not move, and nothing in the data looks wrong. Four places it bites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repricing.&lt;/strong&gt; You list against a level nobody paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buying.&lt;/strong&gt; You pay to a ceiling that was never real, and the margin you modelled was not there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valuation and insurance.&lt;/strong&gt; Values come out high in exactly the categories people insure: graded cards 17.3% and jewellery 16.8% money weighted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything trained on the data.&lt;/strong&gt; A model fed asking prices learns a price level that never cleared.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the accepted figure
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;price&lt;/code&gt; is what the sale closed at, and &lt;code&gt;best_offer&lt;/code&gt; flags which rows were negotiated, so you can measure your own category rather than taking my median for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; https://api.compsapi.com/v1/sold &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"q=charizard psa 10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"days=1095"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"best_offer=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;best_offer=only&lt;/code&gt; returns negotiated sales alone, which is the query that produced the tables above. In Python, the gap on your own data is about six lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.compsapi.com/v1/sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                 &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charizard psa 10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;negotiated sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also an opt in &lt;code&gt;best_offer=verify&lt;/code&gt; mode that returns a &lt;code&gt;best_offer_confidence&lt;/code&gt; of &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;likely&lt;/code&gt;, &lt;code&gt;unlikely&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt; per row, plus the &lt;code&gt;asking_price&lt;/code&gt; and &lt;code&gt;asking_gap_pct&lt;/code&gt; it reasoned from, so you can set your own threshold instead of inheriting mine. It costs extra upstream reads, so it is off by default.&lt;/p&gt;

&lt;p&gt;Free key is 100 requests a month, no card. That is enough to reproduce this on one category.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is 93 searches in 12 categories over 90 days, not a census of eBay. Other categories will differ.&lt;/li&gt;
&lt;li&gt;It measures &lt;strong&gt;accepted Best Offer sales only&lt;/strong&gt;. It says nothing about what share of all eBay sales go through an offer, which varies enormously by category. The 14.9% applies to affected rows, not to your whole dataset.&lt;/li&gt;
&lt;li&gt;Three rows of 5,859 have an accepted price under $5 and are almost certainly an opening bid or a bad parse. Dropping every row under $10 moves the median from 14.9% to 14.8% and the money weighted figure from 14.2% to 14.0%, so nothing rests on them.&lt;/li&gt;
&lt;li&gt;0.4% of pairs read low. I report that rather than filtering it out.&lt;/li&gt;
&lt;li&gt;Where one listing sold several units in the window, the accepted figure is the average of that listing's accepted offers. In these categories almost every listing is a single item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full tables and method: &lt;a href="https://compsapi.com/best-offer-gap" rel="noopener noreferrer"&gt;compsapi.com/best-offer-gap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you maintain something that prices off eBay comps, the useful takeaway is not my number. It is that the error exists, it is one sided, and it is invisible in the response, so it is worth measuring on your own categories rather than assuming it is small.&lt;/p&gt;

</description>
      <category>ebay</category>
      <category>api</category>
      <category>python</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How to get eBay sold listings data in 2026 (findCompletedItems is dead)</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 10 Oct 2026 01:19:48 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/how-to-get-ebay-sold-listings-data-in-2026-findcompleteditems-is-dead-3l2j</link>
      <guid>https://dev.to/sam_compsapi/how-to-get-ebay-sold-listings-data-in-2026-findcompleteditems-is-dead-3l2j</guid>
      <description>&lt;p&gt;If you build anything that needs eBay sold prices, 2026 broke your stack twice. Here is the current state of every route to completed listings data, and working code for the one that still works.&lt;/p&gt;

&lt;p&gt;Full disclosure up front: I run CompsAPI, which is one of the options below. The rest of this post is accurate whether or not you use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;A short timeline, because half the tutorials online are now wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2020&lt;/strong&gt;: eBay deprecates &lt;code&gt;findCompletedItems&lt;/code&gt;, the Finding API call everyone used for sold listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 2025&lt;/strong&gt;: the whole Finding API is decommissioned. Code that called it gets security errors, not data.&lt;/li&gt;
&lt;li&gt;The official replacement, the &lt;strong&gt;Marketplace Insights API&lt;/strong&gt;, is a limited release. You apply with a business case, and approval is restricted to established eBay partners. Most applications go nowhere, and the developer forums are full of people stuck at the same wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid 2026&lt;/strong&gt;: eBay puts the public sold listings filter behind a login. The &lt;code&gt;LH_Sold=1&lt;/code&gt; URL that every scraper and browser extension relied on now redirects to a sign in page for most traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the two free routes, the official API and scraping the public page, are both gone. What is left:&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Terapeak, if a human is doing the looking
&lt;/h2&gt;

&lt;p&gt;eBay's own research tool (now called Product Research, inside Seller Hub) shows up to three years of sold data. If you price items by hand a few times a day, it is genuinely good and costs nothing extra with a seller account.&lt;/p&gt;

&lt;p&gt;It stops at the browser: no API, no export of research results, and sellers report daily caps on queries. The moment you need sold prices in a spreadsheet, a pricing model or an app, there is no door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: scrapers against the logged in page
&lt;/h2&gt;

&lt;p&gt;Some scrapers now run eBay accounts to get through the login wall. Two structural problems. First, the public sold page shows at most 90 days of history. Second, when a sale closes as an accepted Best Offer, the page shows the asking price, not what the buyer actually paid. In offer heavy categories (watches, cards, anything expensive) those numbers run high by 20 to 40 percent per affected row. A scraper inherits both limits because the page itself is the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: a sold listings API
&lt;/h2&gt;

&lt;p&gt;This is what I build. One GET request, JSON back, up to 1,095 days of history per search, and on accepted Best Offer sales the &lt;code&gt;price&lt;/code&gt; field is the real closing amount, flagged per row.&lt;/p&gt;

&lt;p&gt;Get a free key at &lt;a href="https://app.compsapi.com" rel="noopener noreferrer"&gt;app.compsapi.com&lt;/a&gt; (100 searches a month, no card), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://api.compsapi.com/v1/sold"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"q=charizard psa 10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"days=365"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"best_offer=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;239&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charizard PSA 10 Base Set Holo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1450.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"best_offer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"last_sold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-04 18:32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FIXED_PRICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"shipping_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.ebay.com/itm/..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;best_offer: true&lt;/code&gt; row is the point. On the eBay page this sale displays its asking price. Here it is the amount it closed at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python version
&lt;/h2&gt;

&lt;p&gt;There is a zero dependency client (&lt;a href="https://github.com/compsapi/compsapi-python" rel="noopener noreferrer"&gt;github.com/compsapi/compsapi-python&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/compsapi/compsapi-python.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;compsapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompsAPI&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompsAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iphone 14 pro 256gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sales, median $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three years of price history in 15 lines
&lt;/h2&gt;

&lt;p&gt;The thing you could never do with the public page, since it stops at 90 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;compsapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompsAPI&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompsAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;monthly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sale&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_sold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iphone 14 pro 256gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sales)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is a monthly depreciation curve built from real completed sales. Each page of 240 sales costs one request, so a three year pull like this uses about a dozen requests.&lt;/p&gt;

&lt;p&gt;If you want a pre-aggregated answer instead of rows, &lt;code&gt;/v1/comps&lt;/code&gt; returns a cleaned median, the spread and the newest matches in one call, with lots, bundles and wrong model junk filtered before the math runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;eBay US is the primary marketplace, with matching sales from UK, DE, FR, IT, ES, CA and AU merged in or pinned with &lt;code&gt;site=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No seller information on rows.&lt;/li&gt;
&lt;li&gt;The free tier is 100 requests a month. Paid plans start at $9.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you came here from dead code
&lt;/h2&gt;

&lt;p&gt;Migrating from &lt;code&gt;findCompletedItems&lt;/code&gt;: your &lt;code&gt;keywords&lt;/code&gt; becomes &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;EndTimeFrom/To&lt;/code&gt; becomes &lt;code&gt;days&lt;/code&gt;, &lt;code&gt;entriesPerPage&lt;/code&gt; becomes &lt;code&gt;size&lt;/code&gt; (max 240 instead of 100), and the response is flat JSON instead of a SOAP envelope. There is a field by field mapping at &lt;a href="https://compsapi.com/replace/findcompleteditems" rel="noopener noreferrer"&gt;compsapi.com/replace/findcompleteditems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions welcome in the comments. If you hit something the API does not cover, tell me and there is a decent chance it ships.&lt;/p&gt;

</description>
      <category>ebay</category>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
