Every trawl API is plain HTTPS: send a request with your key, get JSON back. One key and one pool of credits works across all of them.
Getting started
Create a key in the console — the free tier includes 250 credits a month, no card needed — then make your first call:
curl "https://api.trawl.dev/ebay/v1/sold?query=iphone+15+pro+256gb&condition=used" \
-H "x-api-key: YOUR_KEY"# pip install trawl-api
from trawl_api import Trawl
client = Trawl() # reads TRAWL_API_KEY
sold = client.ebay.sold("iphone 15 pro 256gb", condition="used")
print(sold.count, sold.results[0].sale_price)// npm install trawl-api
import { Trawl } from 'trawl-api'
const client = new Trawl() // reads TRAWL_API_KEY
const sold = await client.ebay.sold('iphone 15 pro 256gb', { condition: 'used' })
console.log(sold.count, sold.results[0].sale_price)Prefer to poke around first? The playground runs real requests from your browser.
Authentication
Send your key in the x-api-key header on every request. Header names are case-insensitive. Keys are account-wide, and can be deleted at any time from the console.
x-api-key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Never put the key in a query string — query strings end up in server logs, browser history and Referer headers. And never ship it in client-side code: call the API from your backend, with the key in an environment variable. A key that leaks can be deleted and reminted in seconds from the console.
Plans & usage
Your plan includes a monthly allowance of credits shared across all of your API keys — one pool, no per-key budgets — plus a per-second request rate. A call costs one credit; on /sold, several pages cost one credit per page (see Credits & pages), two with full listing details; a call that finds nothing is free. Only successful (2xx) responses spend credits; errors are free, though they still take a slot in your per-second rate. The window follows your billing cycle: it resets when your subscription starts or renews. A plan change keeps the current window: an upgrade raises your allowance at once (what you've used so far stays used), a downgrade takes effect at the renewal. Free accounts reset on the first of each month, UTC. You're welcome to spend the whole month's allowance as fast as your rate allows.
| Plan | Credits / month | Req / sec | Support |
|---|---|---|---|
| Free | 250 | 2 | Email · 24h |
| Basic | 3,000 | 3 | Email · 24h |
| Pro | 15,000 | 5 | Email · 24h |
| Business | 60,000 | 10 | Slack channel |
| Scale | 200,000 | 25 | Slack channel |
| Scale 400k | 400,000 | 25 | Slack channel |
| Scale 600k | 600,000 | 25 | Slack channel |
| Scale 800k | 800,000 | 25 | Slack channel |
| Scale 1M | 1,000,000 | 25 | Slack channel |
Every response reports your standing:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Credits included in your plan per month. |
X-RateLimit-Remaining | Credits left this month. |
X-RateLimit-Reset | When your allowance window resets (Unix timestamp — your billing period end). |
X-Credits-Charged | Credits this response cost — the same number as credits_charged in the body. 0 on an error. |
A 429 means one of two things: going over your per-second rate (the response carries a Retry-After header — wait and retry), or not enough credits left for the request (no Retry-After — lower max_pages, wait for the reset, or upgrade for a bigger pool). Need more than the largest tier? Talk to us about a custom plan.
Credits & pages
Usage is measured in credits. Every successful response tells you what it cost, in a credits_charged field in the JSON body (and an X-Credits-Charged header). Most calls — /item, /categories, and any /sold search that returns a single page — cost 1 credit. The exceptions are a /sold search that returns several pages, and one with full listing details, which costs 2 credits per page.
/sold can return many pages of results in one response. There is no page-by-page fetching. A page is 100 results, and the only thing you choose is how many pages you want: set max_pages (up to 20) and everything arrives together, newest first — up to 2,000 results in a single call. For more, follow the response's next_cursor (below).
curl "https://api.trawl.dev/ebay/v1/sold?query=iphone+15+pro&max_pages=5" \
-H "x-api-key: YOUR_KEY"sold = client.ebay.sold("iphone 15 pro", max_pages=5)const sold = await client.ebay.sold('iphone 15 pro', { max_pages: 5 })max_pages is a ceiling on what the call can cost, not the price. You are charged 1 credit per page of results actually returned — count ÷ 100, rounded up (2 per page with details=1). With max_pages=5 you get up to 500 results, and the most that call can ever cost is 5 credits (10 with details):
| Results returned (count) | Pages filled | credits_charged |
|---|---|---|
| 455 | 5 (the last one part-full) | 5 |
| 385 | 4 | 4 |
| 100 | 1 | 1 |
| 12 | 1 | 1 |
| 0 | none — a successful 200, not charged | 0 |
Full listing details in the same call
A /sold result is the sale: title, price, date, condition. /item is everything else the listing page said — every item specific, the description, all images, seller, shipping, returns. Add details=1 to a search and each result arrives with both: a details object that is exactly what GET /item returns for that item_id, with no second round of calls.
Details cost 2 credits per page. A search with details=1 is sized like one without, in pages of 100, and costs 2 credits per page of results returned instead of 1; nothing if none match.
curl "https://api.trawl.dev/ebay/v1/sold?query=iphone+15+pro&details=1" \
-H "x-api-key: YOUR_KEY"sold = client.ebay.sold("iphone 15 pro", details=True)const sold = await client.ebay.sold('iphone 15 pro', { details: true }) A listing eBay has removed still comes back, because it is a real sale and your comps should not silently lose it. The result carries the sale as usual, with "details": { "listing_state": "removed" } in place of the details.
One thing to know. With details=1, results are limited to the listings /item can answer for — those with full details available, and those eBay has removed; expect fewer results than the same search without it, since full details are available for a portion of sales.
No results, no charge. If your search matches nothing, you get a 200 with "count": 0, "results": [] and "credits_charged": 0. It is a successful request — the search ran, and the answer is that there are no such sales — so don't retry it or treat it as a failure; you simply aren't charged for an answer with nothing in it. The same goes for a /categories lookup that finds nothing. A failure on our side is never a 200: it is a 5xx with an error field, and errors are always free too.
Beyond 2,000: cursors
If count equals max_pages × 100, the response is full and more may exist. It then carries a next_cursor: send it back unchanged as cursor= on the same search and the next pages come back from exactly where the last response stopped. Repeat until next_cursor is null — that was the end. There is no limit to how far a cursor walks: a whole category, a whole year, is reachable one response at a time, and each call is priced like any other — 1 credit per page returned, nothing for the empty response that ends a walk.
curl "https://api.trawl.dev/ebay/v1/sold?category=261328&date_from=2026-10-01&date_to=2026-10-01&max_pages=20" \
-H "x-api-key: $TRAWL_KEY"
# → 2,000 results, "next_cursor": "WzEsMTc…"
curl "https://api.trawl.dev/ebay/v1/sold?category=261328&date_from=2026-10-01&date_to=2026-10-01&max_pages=20&cursor=WzEsMTc…" \
-H "x-api-key: $TRAWL_KEY"
# → the next 2,000, and the next cursor … until "next_cursor": nullcursor = None
while True:
sold = client.ebay.sold(None, category=261328, date_from="2026-10-01", date_to="2026-10-01", max_pages=20, cursor=cursor)
# ... 2,000 results a call
cursor = sold.next_cursor
if cursor is None:
breaklet cursor = null
do {
const sold = await client.ebay.sold(null, { category: 261328, date_from: '2026-10-01', date_to: '2026-10-01', max_pages: 20, cursor })
// ... 2,000 results a call
cursor = sold.next_cursor
} while (cursor) A cursor is a position in the results, not a session: it is never stored on our side and never expires. It is bound to the search that issued it — the same site, query, exclude, category, prices, dates, condition, attr and details — and refused with a 400 on any other; max_pages is the one thing you may change between calls. It cannot be combined with page.
A whole category, no keyword. query is required only when there is no category: a leaf category on its own returns every sale in it, newest first. Add date_from and date_to for a day or a month, and the cursor walks the rest. Before running a request, we check that your remaining credits cover the most it could cost — its max_pages; if they don't, you get a 429 that says so and costs nothing.
Errors
Successful requests return 200 — including a search that matches nothing, which is a successful (and free) empty result, not an error. Everything else returns JSON with a single error field explaining what went wrong:
{ "error": "min_price must be <= max_price" }| Status | Meaning |
|---|---|
| 400 | A parameter failed validation — the message names the field and the rule it broke. |
| 403 | Missing, invalid, or deleted API key. |
| 404 | Nothing found — an unknown path, or an /item whose details are not available yet. Not billed. (A listing eBay has removed is a free 200 with listing_state "removed".) |
| 413 | The request body is larger than 262,144 bytes. Every endpoint is a GET with query parameters; nothing needs a body. |
| 429 | Two cases: a Retry-After header means you hit your plan's per-second rate — wait that long and retry. No Retry-After means your monthly credits are spent, or too few remain to cover the request's max_pages (the message says so) — lower it, upgrade, or wait for X-RateLimit-Reset. Neither costs a credit. |
| 500 | Internal error on our side. |
| 503 | Search backend temporarily unavailable — safe to retry shortly. |
Health
One call tells you whether the API is up and how current its data is: the newest sale you can search and when we collected it. Point an uptime monitor at it. It answers 200 when everything is reachable and 503 when something is not.
https://api.trawl.dev/health Send your key as on any other call. It is free: it spends no credits, still answers when your credits are spent, doesn't count toward your per-second rate and doesn't show in your usage. It allows one request a second; faster gets a 429 with Retry-After: 1.
With no parameters it reports on the whole marketplace. Add category (a leaf category id, the same as on /sold) to report on one category, and site to pick the marketplace: /health?category=261328 is the newest sale in Trading Card Singles on ebay.com. Those two are the only parameters it takes: any other answers a 400 that names it.
{
"ok": true,
"time": "2026-11-06T01:01:37.787Z",
"products": {
"ebay": {
"ok": true,
"site": "EBAY_US",
"category": "261328",
"category_name": "Trading Card Singles",
"last_sale_at": "2026-11-06T00:49:00.000Z",
"last_sale_seconds_ago": 757,
"last_scraped_at": "2026-11-06T00:55:32.053Z",
"last_scraped_seconds_ago": 365
}
}
}| Field | Meaning |
|---|---|
ok | true when every product answered (200), false when one did not (503). Each product carries its own ok too. |
time | When this answer was made, UTC. |
last_sale_at | When the newest searchable sale happened on eBay, UTC. null when there is none to report. |
last_scraped_at | When we collected that same sale, UTC. |
…_seconds_ago | Each of the two, as seconds before this answer, so a monitor can alert on a number. |
category, category_name | The category reported on and its name on that marketplace, only when you passed one. |
The gap between last_sale_at and last_scraped_at is how far behind eBay we were on that sale, usually a few minutes. To watch the feed, poll without a category: something on the marketplace sells every minute, so an old answer there means we are behind. In one category an old last_sale_at can just mean nothing has sold there lately.
eBay API
Sold-listings data: search half a billion real completed eBay sales across the US and UK marketplaces — final price, sale date, condition and shipping for every item that actually sold. eBay itself only shows 90 days of sold history with barely any filtering; this keeps the history and adds the query controls.
Base URL: https://api.trawl.dev/ebay/v1
Overview and live demo: eBay API product page
https://api.trawl.dev/ebay/v1/soldFinds sold listings whose title contains every word in your query, in any order — the same matching eBay's own search uses — or, with a category and no query, every sale in that category. Results are always newest-first, 100 to a page, max_pages sets how many pages come back in the one response, and a full response carries a next_cursor to continue from. The attr filter matches on item specifics without changing the result shape; details=1 adds each result's full listing details, the same data /item returns. Costs 1 credit per page returned, 2 with details; nothing if none match.
Full reference, with every field and code in curl, Python and Node: eBay Sold Listings API
Parameters
queryexcludesitecategoryconditionattrmin_pricemax_pricedate_fromdate_tomax_pagesdetailscursor
https://api.trawl.dev/ebay/v1/itemOne listing's page-level data by item_id: every item specific, the full description, all images, seller detail, shipping and returns, plus that listing's recorded sales. Costs 1 credit. Details become available a few minutes after a sale, so a just-sold item answers 404 until then, and a 404 is never billed. A listing eBay has removed answers a free 200 with listing_state "removed" and nothing else.
Full reference, with every field and code in curl, Python and Node: eBay Sold Item Details API
Parameters
item_idsite
https://api.trawl.dev/ebay/v1/categoriesLook up eBay leaf categories by name, busiest first — use the returned categoryId as the category filter on /sold. Costs 1 credit, nothing if no category matches. Matching covers the category's full path, a numeric query looks up that exact id, and ids differ per marketplace, so pass the same site you'll search with. Note: eBay has no per-brand categories — Pokémon or baseball cards live under generic ones like CCG Individual Cards (183454); narrow to a brand with query terms on /sold.
Full reference, with every field and code in curl, Python and Node: eBay Category IDs API
Parameters
querysite
Guides
One runnable script each, with the output it printed on the date shown on the page.
Comparisons
What each alternative does, row by row, with the sources read and the date.
More APIs coming — same key, same pool, same docs page. Need a custom integration? Contact us.