CompSniper
Migration guide

findCompletedItems replacement: a field-by-field migration guide

eBay decommissioned findCompletedItems on February 4, 2025, and its official successor for sold data, Marketplace Insights, is closed to new developers. The fastest replacement is GET /v1/scrape?sold=true: the same keyword, category, condition, price and date filters, up to 240 sold items per request instead of 100, plus a drop-in adapter below that returns rows in the old findCompletedItems shape.
By Marc Andrew8 minute read

What happened to findCompletedItems

For years, findCompletedItems in eBay's Finding API was how developers got sold and completed listings. eBay decommissioned the whole Finding API on February 4, 2025. The Browse API that replaced it for search returns items currently for sale only, and Marketplace Insights, the one official API with sold items, is a Limited Release that is not accepting new developers. The full history is in does eBay have a sold listings API?

This guide is the practical part: what to change in your code, filter by filter and field by field.

Before and after

# Before: eBay Finding API (decommissioned February 4, 2025). This call now fails.
import requests

params = {
    "OPERATION-NAME": "findCompletedItems",
    "SERVICE-VERSION": "1.13.0",
    "SECURITY-APPNAME": "YOUR-APP-ID",
    "RESPONSE-DATA-FORMAT": "JSON",
    "GLOBAL-ID": "EBAY-US",
    "keywords": "nintendo switch oled",
    "itemFilter(0).name": "SoldItemsOnly",
    "itemFilter(0).value": "true",
    "sortOrder": "EndTimeSoonest",
    "paginationInput.entriesPerPage": "100",
}
r = requests.get("https://svcs.ebay.com/services/search/FindingService/v1", params=params)

Create a free key at compsniper.com/signup (100 requests a month, no credit card) and set it as COMPSNIPER_API_KEY. No eBay developer account, App ID or OAuth flow is needed.

Request filters: old to new

findCompletedItemsCompSniper /v1/scrapeNotes
keywordskeywordSame search text. eBay matching rules apply as before.
categoryIdcategoryIdSame eBay category IDs.
GLOBAL-ID (EBAY-US, EBAY-GB, EBAY-DE, EBAY-FR, EBAY-IT, EBAY-ES, EBAY-ENCA, EBAY-AU)ebaySite (ebay.com, ebay.co.uk, ebay.de, ebay.fr, ebay.it, ebay.es, ebay.ca, ebay.com.au)Marketplace IDs such as EBAY_GB are also accepted.
itemFilter SoldItemsOnly=truesold=true (the default)Every row is a completed sale. Use sold=false for active listings.
itemFilter ConditionitemCondition=new|used or conditionIdconditionId takes eBay's numeric condition IDs (1000, 3000 and so on).
itemFilter MinPrice / MaxPriceminPrice / maxPriceIn the marketplace's currency.
itemFilter ListingType (Auction, FixedPrice)buyingFormat=auction|buyItNow|acceptsOffers
itemFilter LocatedInitemLocation=domestic|worldwideNearest equivalent: domestic keeps sellers in the marketplace's country.
itemFilter EndTimeFrom / EndTimeTosoldAfter / soldBeforeDates as YYYY-MM-DD.
sortOrder EndTimeSoonestsortOrder=endedRecentlyNewest sales first.
sortOrder PricePlusShippingLowest / HighestsortOrder=pricePlusPostageLowest / pricePlusPostageHighest
sortOrder StartTimeNewestsortOrder=timeNewlyListed
paginationInput.entriesPerPage (max 100)count (max 240)Up to 240 sold listings per request, billed as one request.
paginationInput.pageNumberpageRead hasNextPage in the response.
outputSelector SellerInfoAlways includedSeller name, feedback score and positive percent on every row.
SECURITY-APPNAME (App ID)Authorization: Bearer cs_...A key from the dashboard. No eBay developer account or approval.

Invalid values (an unknown sortOrder, a malformed date) return 400 invalid_params with a message naming the parameter. Every parameter is in the API reference.

Response fields: old to new

findCompletedItemsCompSniper
searchResult.item[]items[]
itemIditemId
titletitle
viewItemURLurl
galleryURLthumbnailUrl (fullResThumbnailUrl for a larger image)
primaryCategory.categoryIdcategoryId
sellingStatus.currentPricesoldPrice + soldCurrency (string amount, marketplace currency)
sellingStatus.sellingState = EndedWithSalesNot needed: every sold row is a completed sale
sellingStatus.bidCountbidCount
shippingInfo.shippingServiceCostshippingPrice + shippingCurrency
shippingInfo.shippingTypeshippingType
listingInfo.endTimeendedAt
listingInfo.listingTypelistingType and buyingFormat
listingInfo.bestOfferEnabledbestOfferAccepted (the offer was accepted, not just enabled) and, with bestOfferPrice=true, acceptedOfferPrice
condition.conditionId / conditionDisplayNameconditionId / condition
sellerInfo.sellerUserNamesellerUsername
sellerInfo.feedbackScore / positiveFeedbackPercentsellerFeedbackScore / sellerPositivePercent
location / countryitemLocation
productId (ePID)epid (usually null on sold results)
paginationOutput.totalEntriestotalItems, with hasNextPage
(none)summary: median, p25, p75, mean, min, max and average shipping, calculated for you

Prices are strings in the marketplace's own currency, so an ebay.co.uk search returns GBP and an ebay.de search returns EUR. There is no converted-currency field; convert in your own code if you need one currency.

Keep old code running with a drop-in adapter

If your code reads item["sellingStatus"]["currentPrice"] all over the place, wrap the new call so it returns the old shape. This adapter also asks for real Best Offer prices and puts the price the buyer actually paid into currentPrice when it is known:

Python adapter
# Drop-in adapter: returns rows shaped like findCompletedItems items,
# so code that read item["sellingStatus"]["currentPrice"] keeps working.
import os
import requests

SITES = {"EBAY-US": "ebay.com", "EBAY-GB": "ebay.co.uk", "EBAY-DE": "ebay.de",
         "EBAY-FR": "ebay.fr", "EBAY-IT": "ebay.it", "EBAY-ES": "ebay.es",
         "EBAY-ENCA": "ebay.ca", "EBAY-AU": "ebay.com.au"}

def find_completed_items(keywords, global_id="EBAY-US", page=1, **filters):
    r = requests.get(
        "https://api.compsniper.com/v1/scrape",
        headers={"Authorization": f"Bearer {os.environ['COMPSNIPER_API_KEY']}"},
        params={"keyword": keywords, "ebaySite": SITES[global_id], "sold": "true",
                "count": 240, "page": page, "bestOfferPrice": "true", **filters},
        timeout=90,
    )
    r.raise_for_status()
    data = r.json()
    items = []
    for row in data["items"]:
        paid = row.get("acceptedOfferPrice") or row["soldPrice"]
        items.append({
            "itemId": row["itemId"],
            "title": row["title"],
            "viewItemURL": row["url"],
            "galleryURL": row.get("thumbnailUrl"),
            "primaryCategory": {"categoryId": row.get("categoryId")},
            "sellingStatus": {
                "currentPrice": {"value": paid, "currencyId": row["soldCurrency"]},
                "sellingState": "EndedWithSales",
                "bidCount": row.get("bidCount"),
            },
            "shippingInfo": {"shippingServiceCost": {"value": row.get("shippingPrice"),
                                                     "currencyId": row.get("shippingCurrency")}},
            "listingInfo": {"endTime": row.get("endedAt"),
                            "listingType": row.get("listingType"),
                            "bestOfferAccepted": row.get("bestOfferAccepted")},
            "condition": {"conditionId": row.get("conditionId"),
                          "conditionDisplayName": row.get("condition")},
            "sellerInfo": {"sellerUserName": row.get("sellerUsername"),
                           "feedbackScore": row.get("sellerFeedbackScore"),
                           "positiveFeedbackPercent": row.get("sellerPositivePercent")},
        })
    return {"items": items, "hasNextPage": data.get("hasNextPage"), "summary": data.get("summary")}

Prefer the native response for new code: it is flatter and includes the calculated summary. The JavaScript and TypeScript guide shows the same call in Node.

What you gain over findCompletedItems

  • 240 sold items per request instead of 100, billed as one request.
  • Real Best Offer prices. findCompletedItems only told you an offer was possible. bestOfferPrice=true returns acceptedOfferPrice, what the buyer actually paid, on US-dollar sales at no extra cost. See accepted Best Offer prices.
  • 3 years of history. GET /v1/research/sold goes back up to 3 years, on every plan including free.
  • A price summary in every response: median, 25th to 75th percentile, mean, min, max and average shipping.
  • Optional AI cleanup that removes accessories, parts and wrong models before the median, or relevance=false for eBay's results exactly as listed.
  • Beyond 240: Max Mode pages deeper with a CSV download, and bulk search runs up to 20 keywords at once.
Same window, plus more
The standard sold search covers eBay's recent sold window of about 90 days, like findCompletedItems did. Reach for /v1/research/sold when you need older sales.

Migration checklist

  1. Create a key and store it server-side as COMPSNIPER_API_KEY.
  2. Replace the Finding API URL and App ID with GET /v1/scrape and the Authorization header.
  3. Map your itemFilter values using the request table above. Set ebaySite on every call.
  4. Read items[] and hasNextPage, or drop in the adapter.
  5. Retry 429 and 503 with backoff. Failed requests are not charged. See the error guide.
  6. Cache per keyword for a day; sold prices change daily, not every second.
  7. Optional: add bestOfferPrice=true and switch long-range reports to /v1/research/sold.

findCompletedItems migration questions

Does findCompletedItems still work?

No. findCompletedItems was part of eBay's Finding API, which eBay decommissioned on February 4, 2025. Calls to it now fail, and eBay's Browse API, its official successor for search, returns active listings only.

What is the official replacement for findCompletedItems?

eBay's Marketplace Insights API returns sold items from the last 90 days, but it is a Limited Release that is not open to new developers. Most teams replace findCompletedItems with a third-party sold listings API that works without eBay approval.

How do I migrate findCompletedItems code to CompSniper?

Swap the Finding API call for GET /v1/scrape with your API key, map keywords to keyword, GLOBAL-ID to ebaySite, the sold, condition, price and end-time item filters to sold, itemCondition, minPrice, maxPrice, soldAfter and soldBefore, and read items[] instead of searchResult.item[]. The adapter on this page returns rows in the old shape so existing code keeps working.

Does the replacement return more than findCompletedItems did?

Yes. Up to 240 sold items per request against 100, a calculated median and price range in every response, the real accepted price on Best Offer sales with bestOfferPrice=true, and up to 3 years of sold history on /v1/research/sold, on every plan including free.

How far back does it go?

Like findCompletedItems, the standard sold search covers eBay's recent sold window of about 90 days. For older sales, GET /v1/research/sold returns up to 3 years of eBay sold history with the same key.

Is it free?

There is a free plan with 100 requests a month and no credit card. Paid plans start at $9 a month for 2,000 requests. Each request returns up to 240 sold listings and failed requests are not charged.

eBay API status and CompSniper parameters checked October 14, 2026.

Replace findCompletedItems today

One REST call, up to 240 sold items as JSON, 8 eBay marketplaces, 3-year history and real Best Offer prices. 100 free requests a month.