trawl-api: Python eBay scraper API for sold listings and prices
The official Python client for trawl, a data API for eBay. Search 300+ million completed eBay sales on the US and UK marketplaces and get each one back as typed Python objects: final price, sale date, condition, shipping, seller, item specifics, images and description. One API key, no proxies, no HTML parsing, no browser to run.
pip install trawl-api
Requires Python 3.10 or newer. Create a free account to get an API key; the free plan needs no card.
Quickstart
from trawl_api import Trawl
client = Trawl() # reads the TRAWL_API_KEY environment variable
sold = client.ebay.sold("iphone 15 pro 256gb", condition="used")
for listing in sold.results[:5]:
print(listing.date_sold.date(), listing.sale_price, listing.title)
print(f"{sold.count} results, {sold.credits_charged} credit(s) charged")
The key can also be passed directly: Trawl(api_key="sk_live_..."). Keep it on your server
and out of source control.
What you can ask
| Method | Answers | Costs |
|---|---|---|
client.ebay.sold(query, ...) |
Sold listings matching a query, newest first, up to 2,000 in one response | 1 credit per page of 100 results (2 with details=True); free when nothing matches |
client.ebay.item(item_id) |
One sold listing in full: specifics, seller, every image, description, recorded sales | 1 credit |
client.ebay.categories(query) |
eBay categories by name or id, for the category filter |
1 credit; free when nothing matches |
The full parameter and field reference is at trawl.dev/docs.
Tutorials
Each of these is a complete script in
examples/.
Get the sold prices of a product
from trawl_api import Trawl
client = Trawl()
sold = client.ebay.sold(
"iphone 15 pro 256gb",
condition="used",
exclude=["case", "cracked"], # words that must not be in the title
)
for listing in sold.results:
print(
f"{listing.date_sold:%Y-%m-%d} {listing.currency}{listing.sale_price:.2f} {listing.title}"
)
Average sold price over the last 90 days
max_pages sets how many pages of 100 results come back in the one response, and is also the
most credits the call can cost.
from datetime import date, timedelta
from statistics import mean, median
from trawl_api import Trawl
client = Trawl()
sold = client.ebay.sold(
"nintendo switch oled",
condition="used",
date_from=date.today() - timedelta(days=90),
max_pages=5,
)
prices = [listing.sale_price for listing in sold.results]
print(
f"{len(prices)} sales, average {mean(prices):.2f}, median {median(prices):.2f} {sold.currency}"
)
Filter by price, marketplace and item specifics
sold = client.ebay.sold(
"charizard",
site="EBAY_GB", # ebay.co.uk; prices are then in GBP
min_price=100,
max_price=2000,
attr={"Set": "Base Set", "Grade": ["9", "10"]}, # Grade 9 or 10, from Base Set
)
Export sold listings to CSV or pandas
import csv
from trawl_api import Trawl
COLUMNS = ["date_sold", "title", "sale_price", "currency", "condition", "item_id", "item_link"]
client = Trawl()
sold = client.ebay.sold("charizard base set holo", max_pages=10)
with open("sold.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(file, fieldnames=COLUMNS)
writer.writeheader()
for listing in sold.results:
writer.writerow(listing.model_dump(mode="json", include=set(COLUMNS)))
For pandas: pd.DataFrame(listing.model_dump() for listing in sold.results).
Get one listing's full details
from trawl_api import NotFoundError, Trawl
client = Trawl()
# The item_id of any sold() result; here, the newest sale of a search.
item_id = client.ebay.sold("iphone 15 pro 256gb").results[0].item_id
try:
item = client.ebay.item(item_id)
except NotFoundError:
# Details arrive a few minutes after a sale. A 404 is never billed.
raise SystemExit("Details are not available yet.")
print(item.title, item.sale_price, item.currency)
print(item.specifics.get("Brand")) # item specifics as a dict
if item.seller:
print(item.seller.username, item.seller.feedback_percent)
print(len(item.images), "images")
To get the details of every result of a search in one call, pass details=True to sold()
and read listing.details.
Find a category and search inside it
found = client.ebay.categories("trading card singles", site="EBAY_US")
for category in found.categories:
print(category.category_id, category.name, category.group)
sold = client.ebay.sold("charizard", category=found.categories[0].category_id)
Category ids differ per marketplace, so look them up with the same site you search with.
Credits
Every answer says what it cost, and how much of the month's allowance is left:
sold = client.ebay.sold("rolex submariner", max_pages=3)
sold.credits_charged # 3 when three pages came back, 0 when nothing matched
sold.rate_limit.remaining # credits left in this billing window
sold.rate_limit.reset # when the allowance resets
Errors are never billed.
Errors
import trawl_api
try:
sold = client.ebay.sold("rolex submariner", min_price=2000)
except trawl_api.BadRequestError as error:
print(error) # the API names the field and the rule it broke
except trawl_api.InsufficientCreditsError as error:
print(error) # the month's credits are spent, or too few remain
except trawl_api.RateLimitError as error:
print(error.retry_after) # seconds to wait
except trawl_api.APIStatusError as error:
print(error.status_code, error)
| Exception | When |
|---|---|
BadRequestError |
400, a parameter failed validation |
AuthenticationError |
403, the key is missing, invalid or deleted |
NotFoundError |
404, e.g. an item whose details are not available yet |
RateLimitError |
429 with Retry-After, the plan's per-second rate was exceeded |
InsufficientCreditsError |
429 without Retry-After, not enough credits for the request |
ServerError |
5xx, a failure on trawl's side |
APIConnectionError, APITimeoutError |
the request got no answer |
All of them inherit from trawl_api.TrawlError. Connection errors, 5xx answers and
per-second rate limits are retried twice with backoff before they are raised; change that
with Trawl(max_retries=...). The per-second rate belongs to the account, so keep concurrent
calls within your plan's rate.
Async
import asyncio
from trawl_api import AsyncTrawl
async def main():
async with AsyncTrawl() as client:
sold = await client.ebay.sold("iphone 15 pro 256gb")
print(sold.count)
asyncio.run(main())
Configuration
client = Trawl(
api_key="sk_live_...", # default: the TRAWL_API_KEY environment variable
timeout=60.0, # seconds
max_retries=2,
)
Responses are pydantic models: model_dump() gives a dict,
model_dump_json() a JSON string, and fields the API adds later are kept on the object.
Links
trawl is an independent service and is not affiliated with or endorsed by eBay Inc.
Metadata
Release files for trawl-api 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| trawl_api-0.1.1.tar.gz | 17.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| trawl_api-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.6 kB
Release files / trawl_api-0.1.1.tar.gz
| Download URL | trawl_api-0.1.1.tar.gz |
|---|---|
| Size | 17.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
77e9fa5b0041c4cc7bd39b308b70b413b1d845c72c215c654032c05575b5ef6b
|
|
BLAKE2b-256 checksum How to use checksums |
918570d9f7fd194d64a5b25cd4046bc5465510aa120046eb03154cdb985abed0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / trawl_api-0.1.1-py3-none-any.whl
| Download URL | trawl_api-0.1.1-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
039df3a15ea3b3a44fbc8c0b473cf2e09be0be36b34bd5661c896d343c04820c
|
|
BLAKE2b-256 checksum How to use checksums |
31e753f12beadca13712c7f16d890f884f8a1c482c92ef510049f84d03ddbb1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logSecurity for trawl-api 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for trawl_api-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.