Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Hobby Card Index Data Sales API
version: '1'
summary: Real sold prices, photo identification, cert lookups, pop reports, sold sales and price history for sports cards and trading cards.
description: 'Real sold prices for sports cards and trading cards: a catalog of more than 16 million cards across 25-plus categories (baseball, basketball, football, hockey, soccer, Pokemon, Magic: The Gathering, Yu-Gi-Oh, One Piece and more) with more than 22 million raw and graded prices, refreshed daily, plus card identification from a photo, PSA, BGS, CGC and SGC cert lookups, population reports, verified sold-sale records, daily price history, set lists, market movers, a price watch list with a daily webhook, and bulk price files.
## Keys and plans
Every endpoint needs an API key, sent as the `X-API-Key` header (or `Authorization: Bearer`). Keys come from Settings, API Access, after subscribing. An **Enterprise** plan ($49.99 a month) includes a key for your own business''s internal use: 1 request a second, up to 100,000 a day. Apps, websites and products whose users see the data need a **Commercial plan** (a commercial data license, Terms of Service section 8):
- Lite (plan id `lite`): $99 a month; 25,000 requests, 25,000 photo scans, 5,000 sold-sale lookups, 150 cert lookups, 10 lookups a second, price history.
- Growth Lite (plan id `growth_lite`): $249 a month; 125,000 requests, 125,000 photo scans, 25,000 sold-sale lookups, 750 cert lookups, 15 lookups a second, price history.
- Growth (plan id `growth`): $499 a month; 500,000 requests, 500,000 photo scans, 100,000 sold-sale lookups, 3,000 cert lookups, 20 lookups a second, price history.
- Platform (plan id `platform`): from $1,500 a month; 2,000,000 requests, 1,500,000 photo scans, 500,000 sold-sale lookups, 50,000 cert lookups, 50 lookups a second, price history, bulk price files.
A **free setup month** is by application in Settings, API Access (one per company): enough to build and test against real data, with daily pools. Plans, prices and how to buy, including how an AI agent can buy a plan for its user with no account (GET /api/v1/agent/plans, POST /api/v1/agent/checkout): https://hobbycardindex.com/developers#commercial-plans. Questions or a bigger deal: sean@hobbycardindex.com.
## Credit the data (required)
Commercial plans ask for one link back to https://hobbycardindex.com somewhere on your site, such as ''Prices and API supplied by Hobby Card Index''; it does not need to be beside the prices. Every successful JSON object response carries an `attribution` object (text, url, html, required, terms) and an `X-Attribution` header with the same credit, so it can be rendered straight from the response. Put the credit beside the prices or anywhere on your website, such as the footer. Snippet: `Prices from Hobby Card Index`.
## Conventions
- Base URL `https://hobbycardindex.com`; every path starts with `/api/v1/`. Prices are US dollars with two decimals; a grade with no price is `null`. Grade keys: `loose` (raw), `psa10`, `psa9`, `psa8`, `psa7`, `bgs10` (BGS 9.5), `cgc10`, `sgc10`.
- Successful responses are `{ "success": true, "data": { ... }, "attribution": { ... } }`; errors are `{ "success": false, "error": { "code", "message", "details"? } }` with the HTTP status that fits (400, 401, 402, 403, 404, 409, 413, 429, 500, 503). Streamed downloads (CSV, bulk files, the watch-list prices) are not wrapped.
- Card ids: pass the integer `id` every response carries, or `ext-` plus the card''s `externalId`. Picture links (`imageUrl`) go through our image proxy and need no key; a link can answer 404 later if the picture is withdrawn, so show a placeholder on a 404.
- Rate limits: 1 request a second on an Enterprise key, the plan''s lookups a second on a commercial key, with `RateLimit-*` and `X-RateLimit-*` headers on the one-second window. Commercial plans also return `X-HCI-Plan` and `X-HCI-*-Used` / `X-HCI-*-Allowance` headers for the monthly counters.
- The same data through an MCP server for AI assistants: POST https://hobbycardindex.com/api/v1/mcp, manifest https://hobbycardindex.com/.well-known/mcp.json.'
termsOfService: https://hobbycardindex.com/terms/
contact:
name: Sean Charman, Hobby Card Index
email: sean@hobbycardindex.com
url: https://hobbycardindex.com/developers
license:
name: Hobby Card Index Terms of Service (section 8, Commercial Data License)
url: https://hobbycardindex.com/terms/#data-license
x-logo:
url: https://hobbycardindex.com/hci-logo2.png
altText: Hobby Card Index
href: https://hobbycardindex.com
x-attribution:
text: Prices from Hobby Card Index
url: https://hobbycardindex.com
html: <a href="https://hobbycardindex.com" rel="noopener">Prices from Hobby Card Index</a>
required: true
terms: https://hobbycardindex.com/terms/
servers:
- url: https://hobbycardindex.com
description: Production
security:
- ApiKeyAuth: []
tags:
- name: Sales
description: Verified sold-sale records and active listings.
paths:
/api/v1/prices/{cardId}/sales:
get:
tags:
- Sales
summary: Verified sold sales for one card
operationId: getCardSales
description: 'Individual verified sales: price, date, grade, how it sold (auction, buy_it_now, best_offer), the marketplace, the listing link and the listing photo''s link, plus summary stats for the window. Commercial plans with sold sales (every size; the free setup month includes lookups to test with). Each call returns up to 100 sales in each grade, picked by `sample` (the newest by default), and counts as one sold-sale lookup as well as one request. Other keys get 403 REQUIRES_COMMERCIAL_LICENSE.'
parameters:
- $ref: '#/components/parameters/cardId'
- $ref: '#/components/parameters/days365'
- name: sample
in: query
required: false
description: 'How the 100 sales in each grade are picked: `latest` (the newest), `daily` (one sale per day, the middle-priced sale of that day, newest days first) or `spread` (evenly spaced through the window). Default latest.'
schema:
type: string
enum:
- latest
- daily
- spread
default: latest
- name: everyDays
in: query
required: false
description: 'With sample=daily: one sale per this many days, 1 to 30. Default 1.'
schema:
type: integer
minimum: 1
maximum: 30
default: 1
responses:
'200':
description: Sales in the window, newest first
headers:
X-Attribution:
$ref: '#/components/headers/X-Attribution'
X-HCI-Plan:
$ref: '#/components/headers/X-HCI-Plan'
X-HCI-Requests-Used:
$ref: '#/components/headers/X-HCI-Requests-Used'
X-HCI-Requests-Allowance:
$ref: '#/components/headers/X-HCI-Requests-Allowance'
RateLimit-Limit:
$ref: '#/components/headers/RateLimit-Limit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimit-Remaining'
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/SalesResponse'
attribution:
$ref: '#/components/schemas/Attribution'
required:
- success
- data
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/PaymentRequired'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServerError'
security:
- ApiKeyAuth: []
/api/v1/prices/{cardId}/listings:
get:
tags:
- Sales
summary: Active Buy It Now listings for one card
operationId: getCardListings
description: Listings for sale right now that are this card, deals first (priced under our value at the listing's grade), then the cheapest others, with the seller's feedback. Checked at most every 15 minutes per card. Switched on per account (403 LISTINGS_NOT_ENABLED otherwise), 100 lookups a day (429 LISTINGS_LIMIT_REACHED). Email sean@hobbycardindex.com to have it turned on.
parameters:
- $ref: '#/components/parameters/cardId'
- name: grade
in: query
required: false
description: Only listings at this grade, for example `raw` or `PSA 10`.
schema:
type: string
- name: limit
in: query
required: false
description: Listings to return, 1 to 25. Default 10.
schema:
type: integer
minimum: 1
maximum: 25
default: 10
- name: minFeedbackPercent
in: query
required: false
description: Drop sellers under this feedback percentage. Default 98.
schema:
type: number
minimum: 0
maximum: 100
default: 98
- name: minFeedbackScore
in: query
required: false
description: Drop sellers under this feedback score. Default 50.
schema:
type: integer
minimum: 0
default: 50
responses:
'200':
description: Listings that are this card
headers:
X-Attribution:
$ref: '#/components/headers/X-Attribution'
X-HCI-Plan:
$ref: '#/components/headers/X-HCI-Plan'
X-HCI-Requests-Used:
$ref: '#/components/headers/X-HCI-Requests-Used'
X-HCI-Requests-Allowance:
$ref: '#/components/headers/X-HCI-Requests-Allowance'
RateLimit-Limit:
$ref: '#/components/headers/RateLimit-Limit'
RateLimit-Remaining:
$ref: '#/components/headers/RateLimit-Remaining'
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/ListingsResponse'
attribution:
$ref: '#/components/schemas/Attribution'
required:
- success
- data
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/PaymentRequired'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServerError'
security:
- ApiKeyAuth: []
components:
headers:
X-Attribution:
description: 'Always "Prices from Hobby Card Index (https://hobbycardindex.com)" on a successful JSON object response: the credit the data must carry.'
schema:
type: string
X-HCI-Requests-Used:
description: 'Commercial plans only: requests used this month, after this one.'
schema:
type: integer
X-HCI-Requests-Allowance:
description: 'Commercial plans only: the plan''s monthly requests.'
schema:
type: integer
RateLimit-Remaining:
description: Requests left in the current one-second window.
schema:
type: integer
X-HCI-Plan:
description: 'Commercial plans only: the plan id behind this key.'
schema:
type: string
RateLimit-Limit:
description: Requests allowed in the current one-second window (X-RateLimit-* are sent too).
schema:
type: integer
parameters:
days365:
name: days
in: query
required: false
description: Lookback window in days, 1 to 365. Default 90. A longer window is trimmed to 365 (the reply's periodDays says which window was used).
schema:
type: integer
minimum: 1
maximum: 365
default: 90
cardId:
name: cardId
in: path
required: true
description: The card's `id` from search, scan, movers or watch-list results (an integer), or `ext-` followed by its `externalId` (for example `ext-67890`). Letters, digits, dashes, underscores and dots, up to 150 characters.
schema:
type: string
maxLength: 150
pattern: ^[a-zA-Z0-9\-_.]+$
schemas:
Attribution:
type: object
properties:
text:
type: string
const: Prices from Hobby Card Index
url:
type: string
format: uri
const: https://hobbycardindex.com
html:
type: string
const: <a href="https://hobbycardindex.com" rel="noopener">Prices from Hobby Card Index</a>
description: 'Ready to paste: the credit as a link.'
required:
type: boolean
const: true
terms:
type: string
format: uri
const: https://hobbycardindex.com/terms/
required:
- text
- url
- html
- required
- terms
description: 'The credit this data must carry wherever it is shown: a visible "Prices from Hobby Card Index" link to https://hobbycardindex.com. Added to every successful JSON object response (also sent as the X-Attribution header).'
CardRef:
type: object
properties:
id:
type: integer
description: The catalog id. Pass it to the other card endpoints.
externalId:
type:
- string
- 'null'
description: Our catalog's secondary id for the card. Accepted by the card endpoints as `ext-<externalId>`.
name:
type: string
description: 'The card name, with the parallel in brackets when it is not the base card, for example `Mike Trout [Gold] #US175`.'
year:
type:
- integer
- string
- 'null'
description: The year the card came out (for Pokemon, the year its set was released); null when unknown.
setName:
type:
- string
- 'null'
description: The set the card belongs to.
category:
type:
- string
- 'null'
description: The category, for example Baseball or Pokemon.
playerName:
type:
- string
- 'null'
description: The player or character on the card.
required:
- id
- name
description: The card block the sales, listings, history and population endpoints return.
Sale:
type: object
properties:
soldAt:
type:
- string
- 'null'
format: date-time
description: When the sale closed.
price:
type: number
description: What was paid, in US dollars.
grade:
type:
- string
- 'null'
description: The grade as listed, for example `PSA 10`; null for a raw card.
gradingCompany:
type:
- string
- 'null'
description: PSA, BGS, SGC, CGC or null.
title:
type:
- string
- 'null'
description: The listing title.
auction:
type: boolean
description: true when the sale was an auction. `saleType` carries the full answer.
saleType:
type:
- string
- 'null'
enum:
- auction
- buy_it_now
- best_offer
- null
description: How the item sold; null when the marketplace did not say.
askingPrice:
type:
- number
- 'null'
description: On best_offer sales, the asking price; `price` is the accepted offer.
bids:
type:
- integer
- 'null'
description: Bid count on an auction, when reported.
source:
type: string
description: The marketplace it sold on, for example ebay, tcgplayer, fanatics-collect, goldin, heritage, or marketplace.
url:
type:
- string
- 'null'
description: The listing page.
format: uri
imageUrl:
type:
- string
- 'null'
description: The listing photo's link (not a copy of the photo).
format: uri
required:
- price
- auction
- source
Listing:
type: object
properties:
itemId:
type:
- string
- 'null'
description: The marketplace item id.
title:
type: string
description: Listing title.
price:
type: number
description: Item price in US dollars.
shipping:
type:
- number
- 'null'
description: Shipping cost when the marketplace reported it.
total:
type:
- number
- 'null'
description: price + shipping, when shipping is known.
currency:
type: string
const: USD
grade:
type: string
description: '`raw`, `graded`, or the grader and grade, for example `PSA 10`.'
condition:
type:
- string
- 'null'
description: The listing's condition text.
marketValue:
type:
- number
- 'null'
description: Our price for the card at the listing's grade.
belowMarketPct:
type:
- number
- 'null'
description: How far under marketValue the listing is, as a percentage.
seller:
type: object
properties:
username:
type:
- string
- 'null'
description: Seller name.
feedbackPercent:
type:
- number
- 'null'
description: Seller feedback percentage.
feedbackScore:
type:
- integer
- 'null'
description: Seller feedback score.
topRated:
type: boolean
description: Top-rated seller.
imageUrl:
type:
- string
- 'null'
description: The listing photo's link.
format: uri
url:
type:
- string
- 'null'
description: The listing, with our affiliate tag.
format: uri
required:
- title
- price
- grade
- url
Error:
type: object
properties:
success:
type: boolean
const: false
description: false on every error the API itself sends. The per-second rate limiter's 429 body has only `error`.
error:
type: object
properties:
code:
type: string
description: Machine-readable code, for example VALIDATION_ERROR, INVALID_PARAM, MISSING_API_KEY, INVALID_API_KEY, KEY_REVOKED, PLAN_REQUIRED, TRIAL_ENDED, NOT_ON_PLAN, REQUIRES_COMMERCIAL_LICENSE, REQUIRES_GROWTH, REQUIRES_PARTNER_PLAN, RATE_LIMITED, MONTHLY_LIMIT_REACHED, DAILY_LIMIT_REACHED, NOT_FOUND, SERVER_ERROR.
message:
type: string
description: What went wrong and, where there is one, what to do about it.
details:
type: object
additionalProperties: true
description: 'Extra fields on some errors: plans_url, upgrade_url, limit, allowance, plan, upgrade.'
required:
- code
- message
required:
- error
description: The error envelope from every endpoint.
ListingsResponse:
type: object
properties:
card:
$ref: '#/components/schemas/CardRef'
status:
type: string
enum:
- ok
- no-listings
- parallel
- no-price
- budget
description: ok; no-listings; parallel (parallel cards are not matched yet); no-price (no market value to check against); budget (marketplace lookups paused for today, try later).
checkedAt:
type:
- string
- 'null'
format: date-time
description: When the listings were last checked (cached up to 15 minutes).
count:
type: integer
listings:
type: array
items:
$ref: '#/components/schemas/Listing'
description: 'Buy It Now listings that are this card: deals first, then the cheapest others.'
usage:
type: object
properties:
usedToday:
type: integer
limitPerDay:
type: integer
required:
- card
- status
- count
- listings
SalesResponse:
type: object
properties:
card:
$ref: '#/components/schemas/CardRef'
periodDays:
type: integer
description: The lookback window that was applied.
sample:
type: string
enum:
- latest
- daily
- spread
description: How the sales were picked.
everyDays:
type: integer
description: 'With sample=daily: the days each sale stands for.'
summary:
type: object
properties:
count:
type: integer
average:
type:
- number
- 'null'
description: Average sale price in the window.
low:
type:
- number
- 'null'
description: Lowest sale price in the window.
high:
type:
- number
- 'null'
description: Highest sale price in the window.
required:
- count
sales:
type: array
items:
$ref: '#/components/schemas/Sale'
description: 'Verified sales, newest first: the newest 100 in each grade for metered plans.'
required:
- card
- periodDays
- summary
- sales
responses:
NotFound:
description: 'NOT_FOUND: no card (or file) matches.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: 'TRIAL_ENDED: the free setup month is over and the first month of the plan is not paid yet.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: 'The key''s plan does not include this: PLAN_REQUIRED, DEALER_KEY_SCOPE, NOT_ON_PLAN, REQUIRES_COMMERCIAL_LICENSE, REQUIRES_GROWTH, REQUIRES_PARTNER_PLAN, REQUIRES_PLATFORM_PLAN, REQUIRES_PRICE_FILES, REQUIRES_ENTERPRISE, LISTINGS_NOT_ENABLED. The message says which plan has it and `details.plans_url` where to get it.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
TooManyRequests:
description: RATE_LIMITED (the per-second window; slow down and retry), RATE_LIMIT_EXCEEDED (the daily ceiling), MONTHLY_LIMIT_REACHED, DAILY_LIMIT_REACHED, SALES_LIMIT_REACHED, CERT_LIMIT_REACHED, POP_LIMIT_REACHED, LISTINGS_LIMIT_REACHED, SCAN_ENGINE_BUSY (Retry-After says when).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ServerError:
description: 'SERVER_ERROR (500 or 503): something failed on our side; retry in a minute.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: 'The request is not valid: VALIDATION_ERROR, INVALID_PARAM, BAD_REQUEST, SEARCH_TOO_BROAD, NO_IMAGE, INVALID_IMAGE.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: No key (MISSING_API_KEY), a key we do not know (INVALID_API_KEY) or one that was revoked (KEY_REVOKED).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: 'Your HobbyCardIndex API key (hci_live_...). `Authorization: Bearer <key>` is accepted too. Keys: https://hobbycardindex.com/developers.'
externalDocs:
description: Developer documentation with examples in curl, JavaScript and Python
url: https://hobbycardindex.com/developers