Changelog
New fields, capabilities, and improvements. We ship every few days.
CSV export now returns rate-limit headers
No breaking changes. Headers appear automatically on all CSV responses.
GET /v1/market/sales/export/csv now returns X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers alongside the file download — the same headers you already see on JSON responses.
X-RateLimit-Remaining reflects your remaining daily budget after the download is charged, so you can tell exactly how much headroom you have left without making an extra API call.
| Field | Description |
|---|---|
| X-RateLimit-Limit | Your total daily row budget (e.g. 100000 on Starter). "unlimited" for plans without a cap. |
| X-RateLimit-Remaining | Rows remaining for today after this download. Deducted at the time the file is served. |
| X-RateLimit-Reset | Unix timestamp (UTC) when the budget resets — midnight UTC of the current day. |
GET /v1/market/sales/export/csv?q=ohtani&date_from=2026-01-01
-H "x-market-api-key: tca_..." -o ohtani_sales.csv
Response headers:
X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 62000
X-RateLimit-Reset: 1751155200One daily budget — 100K rows, shared across JSON calls and CSV downloads
Starter: 100K rows/day · JSON calls and CSV downloads share the same budget · no breaking changes
Starter plan now has a single shared daily budget of 100,000 rows. Every row you receive counts toward the same pool — whether it comes from a paginated JSON call or a CSV file download. Pull 60,000 rows today and you have 40,000 left, available as more API calls or a CSV download.
CSV downloads are now counted toward your budget. When you call GET /v1/market/sales/export/csv, the number of rows in the file is deducted from your daily total. Check X-RateLimit-Remaining after any call — it always reflects your true remaining budget.
| Field | Description |
|---|---|
| GET /sales | Each row returned counts against your daily budget. X-RateLimit-Remaining updates after every response. |
| GET /sales/export/csv | Rows in the downloaded file count against the same daily budget. Same filter params as /sales. |
| X-RateLimit-Remaining | Your remaining rows for the day — updated after every JSON call and every CSV download. |
// JSON call: 1,000 rows returned
GET /v1/market/sales?q=ohtani&limit=1000
→ X-RateLimit-Limit: 100000
→ X-RateLimit-Remaining: 99000
// CSV download: 40,000 rows in the file
GET /v1/market/sales/export/csv?q=ohtani&date_from=2026-01-01
-H "x-market-api-key: tca_..." -o ohtani_sales.csv
// Next JSON call: budget reflects both
→ X-RateLimit-Remaining: 59000Webhooks add-on — $10/mo on Starter, 7-day free trial
$10/mo · Starter add-on · Up to 5 endpoints · 7-day free trial · HMAC-SHA256 signed · 3 retries with backoff
Webhooks are now available as a self-serve add-on for Starter subscribers. Register up to 5 HTTPS endpoints and we'll push every new card sale to them within ~30 seconds of indexing — no polling loop required.
Each delivery is a signed batch of up to 1,000 records in the same schema as GET /sales. We retry 3 times with backoff (0s / 5s / 30s) on failure. If all three fail, your cursor holds and you pick up exactly where you left off on the next cycle — no missed records, no duplicates.
Webhooks do not include NRT (near-real-time eBay data). NRT remains a separate add-on available on Custom plans.
| Field | Description |
|---|---|
| POST /webhook | Add an endpoint (max 5). Required: url, secret. Optional: label. |
| GET /webhook | List all active endpoints for your key. |
| DELETE /webhook/{id} | Remove an endpoint by ID. |
| X-Webhook-Signature | HMAC-SHA256 signature on every delivery. Verify against the raw request body. |
// Register an endpoint
POST /v1/market/webhook
{ "url": "https://your-server.com/hook", "secret": "your-secret", "label": "prod" }
// Delivery (POSTed to your URL every ~30s when new sales exist)
{
"event": "sales.batch",
"timestamp": "2026-06-20T10:00:00Z",
"count": 83,
"data": [ { "id": "ebay-...", "price": 312.00, ... } ]
}Cursor pagination on all plans · CSV export on Starter
Cursor pagination on all plans including Free · CSV export on Starter and above · no breaking changes
Cursor pagination is now available on every plan, including Free and Starter. Every response from /v1/market/sales now includes a next_cursor field in the pagination object when more results exist. Pass it as ?cursor= on your next request to fetch the next page — no offset drift, no missed or duplicated records as new data is indexed.
CSV export is now available on Starter and above. Hit GET /v1/market/sales/export/csv with the same filter params you'd use on /sales and receive a download-ready CSV file. Rows count against your shared daily budget (100K on Starter).
Both features require no integration changes if you're already on the API — just start using the new parameters.
| Field | Description |
|---|---|
| next_cursor | Opaque string in pagination object. Present when has_more=true. Pass as ?cursor= on the next request. |
| cursor | Request param. Pass next_cursor from a previous response to continue from exactly where you left off. |
| GET /sales/export/csv | Same filter params as /sales. Returns a CSV file download. Rows count against the shared daily budget. |
// Cursor pagination — page through large result sets
GET /v1/market/sales?q=psa+10+trout&limit=1000
→ pagination.next_cursor: "eyJwZ19pZCI6IDE4N..."
GET /v1/market/sales?q=psa+10+trout&limit=1000&cursor=eyJwZ19pZCI6IDE4N...
→ pagination.next_cursor: null // last page
// CSV download
GET /v1/market/sales/export/csv?q=ohtani&date_from=2026-01-01
→ CSV file downloadMajor auction house results — 175,000+ lots, 2012 to today
175,000+ lots · 3 auction houses · 2012–today · hammer price (add ~22% for all-in buyer cost) · use ?platform= to filter by house.
Sales data from three major card auction houses is now live in the API. Results appear automatically in any search alongside eBay data — no integration changes required. Use ?platform= to query a specific house or omit it entirely to search across all sources at once.
The combined dataset covers 175,000+ completed auction lots from 2012 through today — over a decade of high-value card sales. It includes bid counts, lot images, direct links to each lot page, and parsed grade/grader fields for slabbed cards. This is the same underlying data serious collectors and dealers use to establish market value.
One important distinction: auction house prices are hammer prices — the amount called at close by the auctioneer. Buyers pay an additional buyer's premium on top (typically 20–22%). eBay prices in the API are all-in. If you're comparing prices across platforms, account for this difference.
| Field | Description |
|---|---|
| platform | Filter to a specific auction house with ?platform=<name>. Omit to search across all platforms. |
| price | Hammer price for auction house lots — not all-in. Buyers pay an additional ~20–22% buyer's premium on top. |
| sold_at | Exact UTC close time — 100% populated for auction house lots. |
| listing_url | Direct link to the lot page. |
| feedback | Always null — not applicable for auction houses. |
// Search across all platforms (eBay + auction houses)
GET /v1/market/sales?q=psa+10+mickey+mantle
// Auction house record
{
"platform": "goldin",
"listing_type": "auction",
"title": "1952 Topps #311 Mickey Mantle — PSA VG-EX 4",
"sale_date": "2024-03-17",
"sold_at": "2024-03-17T02:30:00Z",
"price": 75000.00,
"bids": 18,
"grader": "PSA",
"grade": "4",
"price_confirmed": true
}shipping_price — buyer shipping cost, separate from sale price
~67% of records have shipping_price populated. ?shipping_max=0 for free shipping only.
Every sale response now includes shipping_price — the amount the buyer paid for shipping, separate from the card price. A free-shipping listing returns 0.00. When shipping cost was not disclosed by the seller, the field is null.
Filter by shipping cost with ?shipping_max=0 (free shipping only) or ?shipping_max=10 (shipping ≤ $10). Combine with price filters to compare true all-in costs across listings.
Coverage is approximately 67% of records. The remaining ~33% of listings did not publish shipping cost and return null.
| Field | Description |
|---|---|
| shipping_price | number|null. Buyer shipping cost in USD. 0.00 = free shipping. Null when not disclosed by the seller. |
| shipping_max | Filter param. Only return records with shipping_price ≤ this value. Use shipping_max=0 for free shipping only. Records with null shipping_price are excluded. |
// Free shipping only
GET /v1/market/sales?q=psa+10+mike+trout&shipping_max=0
// ≤ $10 shipping
GET /v1/market/sales?q=psa+10+ohtani&shipping_max=10
// Response (each record)
{
"id": "123456789",
"price": 312.00,
"shipping_price": 0.00,
...
}Webhooks — delivery daemon deployed, multi-endpoint fanout
Auction close → delivery: 9 min median · 11 min 90th pct. Every POST signed with HMAC-SHA256.
The webhook delivery daemon shipped. It polls every 30 seconds and fans out to all registered endpoints per API key. Each batch is HMAC-SHA256 signed and retried three times with backoff (0s / 5s / 30s) on failure.
This update also added support for multiple endpoints per key (up to 5) via a new webhook_endpoints table — moving from a single URL column to a normalized per-endpoint model.
| Field | Description |
|---|---|
| event | "sales.batch" — type identifier for the delivery |
| timestamp | ISO 8601 UTC timestamp of the delivery |
| count | Number of records in this batch (up to 1,000) |
| data | Array of sale records — same schema as /v1/market/sales |
POST https://your-endpoint.com/webhook
Content-Type: application/json
X-Webhook-Signature: sha256=<hmac-sha256>
{
"event": "sales.batch",
"timestamp": "2026-06-14T06:09:00Z",
"count": 47,
"data": [
{
"id": "ebay-123456789",
"price": 312.00,
"listing_type": "auction",
"sold_at": "2026-06-14T06:00:24Z",
"player": "Shohei Ohtani",
...
}
]
}Card catalog — 13.8M cards, sets, and metadata
The catalog API is now in beta. Search and browse 13.8M cards across sports, years, sets, and player names — with structured flags for rookies, autos, relics, short prints, print runs, and more. Card images are served from our own CDN.
The catalog is a complement to the sales API, not a replacement. Use it to build structured lookups, enrich your own records with card metadata, or power a type-ahead search by player or set name. The Catalog API is an add-on available to any plan. Access is currently invite-only while we finalize pricing. Email hello@thecardapi.com to join the beta.
| Field | Description |
|---|---|
| GET /v1/catalog | Search and filter cards by set, player, sport, year, variant flags |
| GET /v1/catalog/{id} | Single card by ID |
| GET /v1/catalog/sets | Browse sets — filterable by sport, year, name |
| GET /v1/catalog/sets/{id} | Single set with card count and image coverage |
| GET /v1/catalog/sports | List of distinct sports in the catalog |
{
"id": 1042981,
"set_id": 4821,
"sport": "Baseball",
"year": 2011,
"set_name": "2011 Topps Update Series",
"card_number": "US175",
"subject": "Mike Trout",
"is_rookie": true,
"is_auto": false,
"print_run": null,
"image_url_front": "https://cdn.thecardapi.com/cards/1042981_front.jpg",
"has_front_image": true
}print_run field — filter by serial run, retrieve structured print-run value
~36% of records have print_run populated. /99 → 99. 1/1 → 1. Base cards → null.
Every sale response now includes a print_run field — the denominator of the serial fraction stamped on the card. A card serialized /99 returns "print_run": 99. A 1/1 returns "print_run": 1. A base card with no serial number returns null.
Filter results directly with ?print_run_max=99 (any card numbered /99 or lower) or ?print_run_min=1 (1/1s only). Combine both to target a range — e.g. print_run_min=1&print_run_max=10 for ultra-rare single-digit print runs.
Approximately 36% of all records (6.2 million rows) have print_run populated. Coverage grows automatically as historical data backfills.
| Field | Description |
|---|---|
| print_run | Integer or null. Denominator of the serial fraction. /99 → 99. /10 → 10. 1/1 → 1. Null for unlisted base cards. |
| print_run_min | Filter param. Only return records with print_run ≥ this value (inclusive). |
| print_run_max | Filter param. Only return records with print_run ≤ this value (inclusive). |
// Cards serialized /10 or lower
GET /v1/market/sales?q=mike+trout&print_run_max=10
// Response (each record)
{
"id": "ebay-387214905012",
"title": "2011 Topps Update Mike Trout RC /10 BGS 9.5",
"price": 14500.00,
"listing_type": "auction",
"sold_at": "2026-06-14T04:22:11Z",
"print_run": 10
}10 structured metadata fields added to the response schema
Every response from /v1/market/sales now includes 10 structured fields — player, sport, team, manufacturer, card_set, card_number, year, season, league, and features. These fields are populated where available from the original listing data. They will be null (or [] for features) when not present.
Coverage is currently limited to recent sales where structured listing data was provided by the seller. Coverage grows automatically as more listings include this data — no integration changes are required.
| Field | Description |
|---|---|
| player | Player name |
| sport | Sport (Baseball, Basketball, Hockey, …) |
| team | Team name at time of card printing |
| manufacturer | Card manufacturer (Topps, Panini, Bowman, …) |
| card_set | Set name (e.g. 2022 Topps Update Series) |
| card_number | Set card number (e.g. #295 or BCP-42) |
| year | Card year |
| season | Sports season (e.g. 2024-25) |
| league | League (e.g. Major League (MLB), NBA) |
| features | Array of card attributes. e.g. ["Auto", "Rookie", "Patch"]. Empty array when not provided. |
{
"player": "Bobby Witt Jr.",
"sport": "Baseball",
"team": "Kansas City Royals",
"manufacturer": "Topps",
"card_set": "2022 Topps Update Series",
"card_number": "SMLB-82",
"year": 2022,
"season": "2022",
"league": "Major League (MLB)",
"features": ["Insert", "Rookie"]
}Print-run and serial-number search — exact matching, no false positives
Previously /10 matched PSA 10 grades. Previously 47/100 matched any record containing 47. Both fixed.
Searching for /10 now returns cards from a 10-copy print run — not PSA 10 grades. Searching for 47/100 now matches the exact serial-numbered card stamped 47 out of 100, not any record containing the number 47.
Two distinct search intents are handled separately: N/M (e.g. 47/100) targets the specific physical card with that serial stamp. /N (e.g. /10) matches any card from that print run. Both work identically in the playground and the market API.
price_confirmed — distinguish confirmed prices from early captures
Every record now includes price_confirmed. true means the price is the verified final transaction amount. false means the record was captured very shortly after close and the final price will be confirmed on our next daily run (within 24 hours).
Most use cases can ignore this. It matters most if you are building something that processes data continuously and needs to distinguish between an early capture and a settled transaction.
| Field | Description |
|---|---|
| price_confirmed | true = verified final price. false = early capture, confirmed price available within 24 hours. |
3–5ms search across 17 million records
683ms → 3–5ms. No API changes required.
A full-text sales search — e.g. 'PSA 10 Mike Trout 2011 Topps Update' filtered by grade, grader, and date range — now returns in 3–5ms across 17 million records. The same query previously took 683ms.
This was a complete rewrite of the query backend. No API changes were required. All existing integrations benefit automatically.
Coming soon
- →Additional auction houses — PWCC, Heritage, and more
- →Historical bulk exports — full dataset downloads for companies that need years of data at once
- →Per-card price history — time-series comps by catalog ID
Questions or feedback? hello@thecardapi.com