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 Scan 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: Scan
description: Card identification from a photo (commercial plans).
paths:
/api/v1/scan:
post:
tags:
- Scan
summary: Identify a card from a photo
operationId: scanCard
description: 'Send one photo as base64 and get the card: the best match with its prices and a confidence from 0 to 1, up to 7 alternatives, and on a graded card the label read into `slab` (grader, grade, cert number, whether the cert verified, the price at that grade). Our own image index answers first; a miss falls through to a vision model within the plan''s share. Commercial plans only (403 REQUIRES_PARTNER_PLAN on an Enterprise key), counted as a scan against the monthly scans, with its own per-second limit. A request without a usable photo is not charged; a bad photo is. 429 SCAN_ENGINE_BUSY carries Retry-After.'
requestBody:
required: true
description: The photo and optional hints. Up to about 14 MB of JSON.
content:
application/json:
schema:
$ref: '#/components/schemas/ScanRequest'
responses:
'200':
description: The identification
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'
X-HCI-Scans-Used:
description: Scans used this month, after this one.
schema:
type: integer
X-HCI-Scans-Allowance:
description: The plan's monthly scans.
schema:
type: integer
Retry-After:
$ref: '#/components/headers/Retry-After'
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/ScanResponse'
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'
'413':
description: 'IMAGE_TOO_LARGE: the photo is over 10 MB.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServerError'
security:
- ApiKeyAuth: []
/api/v1/scan/{scanId}/feedback:
post:
tags:
- Scan
summary: Tell us what the user said about a scan
operationId: scanFeedback
description: 'The user''s answer to a scan: `correct` (our match was the card), `picked` (they chose another card, `cardId`) or `none` (none of our cards was it). A later answer replaces an earlier one, up to 30 days after the scan. Counts as one request, not a scan. 404 NOT_FOUND when the scan is not this account''s or the card does not exist; 409 FEEDBACK_WINDOW_CLOSED after 30 days.'
parameters:
- name: scanId
in: path
required: true
description: The scanId the scan returned.
schema:
type: integer
requestBody:
required: true
description: The answer.
content:
application/json:
schema:
type: object
properties:
result:
type: string
enum:
- correct
- picked
- none
description: What the user said.
cardId:
type:
- integer
- string
description: 'With picked: the card they chose, usually an `id` from `alternatives`.'
required:
- result
responses:
'200':
description: Saved
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:
type: object
properties:
scanId:
type: integer
result:
type: string
enum:
- correct
- picked
- none
cardId:
type:
- integer
- 'null'
description: The chosen card, with picked.
recordedAt:
type: string
format: date-time
required:
- scanId
- result
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'
'409':
description: 'FEEDBACK_WINDOW_CLOSED: the scan is more than 30 days old.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServerError'
security:
- ApiKeyAuth: []
components:
schemas:
ScanRequest:
type: object
properties:
image:
type: string
description: The photo as base64 (JPEG, PNG or WebP, under 10 MB). A `data:` URL prefix is fine. One card per photo.
contentEncoding: base64
mimeType:
type: string
description: The photo's media type. Default image/jpeg.
maxLength: 40
default: image/jpeg
category:
type: string
description: Narrow the match to one category, for example football, pokemon or marvel.
maxLength: 40
slab:
type: boolean
description: true for a graded slab (the label is read in full), false for a raw card (no label read). Leave it out and a quick check decides.
required:
- image
Slab:
type: object
properties:
grader:
type:
- string
- 'null'
description: PSA, BGS, CGC, SGC, TGA or null.
grade:
type:
- number
- string
- 'null'
description: The numeric grade, `Authentic`, or null when unread.
autoGrade:
type:
- string
- 'null'
description: Autograph grade when the label carries one.
certNumber:
type:
- string
- 'null'
description: The cert number read from the label or the grader's record.
certVerified:
type: boolean
description: true when the grader's record for that cert was found and agrees with the label.
priceAtGrade:
type:
- number
- 'null'
description: The card's price at exactly this grade when we price that grade; null otherwise (never a neighbouring grade's price).
verifyUrl:
type:
- string
- 'null'
description: The grader's own page for the cert (PSA, CGC and TGA open the cert; Beckett and SGC open their lookup page).
format: uri
frontImageUrl:
type:
- string
- 'null'
description: The grader's front slab photo through our image link, when they have one.
format: uri
backImageUrl:
type:
- string
- 'null'
description: The grader's back slab photo through our image link, when they have one.
format: uri
required:
- certVerified
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).'
ScanCard:
type: object
properties:
id:
type:
- integer
- 'null'
description: The catalog id, or null when the match is not in our catalog yet.
externalId:
type:
- string
- 'null'
description: Secondary id.
name:
type:
- string
- 'null'
description: Card name; a parallel is named in brackets.
playerName:
type:
- string
- 'null'
description: Player or character.
year:
type:
- integer
- string
- 'null'
setName:
type:
- string
- 'null'
description: Set name.
cardNumber:
type:
- string
- 'null'
description: Card number.
printRun:
type:
- integer
- 'null'
description: Print run for numbered cards.
parallel:
type: string
description: The parallel, or `Base`.
category:
type:
- string
- 'null'
description: Category.
imageUrl:
type:
- string
- 'null'
description: Picture through our image link, or null.
format: uri
confidence:
type:
- number
- 'null'
description: 0 to 1, three decimals.
Prices:
allOf:
- $ref: '#/components/schemas/PriceGrid'
- type: object
properties:
volume:
type:
- integer
- 'null'
description: Recent sales volume, a count.
updated:
type:
- string
- 'null'
format: date-time
description: When this card's prices were last refreshed.
fromSales:
type: array
items:
type: string
description: Grades (keys of this object) whose price came from our own recent sold sales because no other price was on file for that grade.
description: 'The price block of GET /api/v1/prices/{cardId}: the grade grid plus volume and the last update.'
PriceGrid:
type: object
properties:
loose:
type:
- number
- 'null'
description: raw (ungraded) price in US dollars; null when there is no price at that grade.
psa10:
type:
- number
- 'null'
description: PSA 10 price in US dollars; null when there is no price at that grade.
psa9:
type:
- number
- 'null'
description: PSA 9 price in US dollars; null when there is no price at that grade.
psa8:
type:
- number
- 'null'
description: PSA 8 price in US dollars; null when there is no price at that grade.
psa7:
type:
- number
- 'null'
description: PSA 7 price in US dollars; null when there is no price at that grade.
bgs10:
type:
- number
- 'null'
description: BGS 9.5 (BGS 10 when there is no 9.5 price) price in US dollars; null when there is no price at that grade.
cgc10:
type:
- number
- 'null'
description: CGC 10 price in US dollars; null when there is no price at that grade.
sgc10:
type:
- number
- 'null'
description: SGC 10 price in US dollars; null when there is no price at that grade.
description: Current prices by grade, in US dollars with two decimals. A grade with no price is null; `bgs10` holds the BGS 9.5 price.
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.
ScanResponse:
type: object
properties:
match:
oneOf:
- allOf:
- $ref: '#/components/schemas/ScanCard'
- type: object
properties:
prices:
oneOf:
- $ref: '#/components/schemas/Prices'
- type: 'null'
- type: 'null'
description: The best card, with its prices; null when nothing is close enough.
alternatives:
type: array
items:
$ref: '#/components/schemas/ScanCard'
description: Up to 7 more cards, without prices. Show them and let the user choose.
engine:
type: string
description: '`image` when our own image index answered, `vision` when the fallback model did.'
slab:
oneOf:
- $ref: '#/components/schemas/Slab'
- type: 'null'
description: The graded-card label, or null on a raw card.
scanId:
type:
- string
- integer
- 'null'
description: Our id for this scan.
uncertain:
type: boolean
description: True when the user should confirm the card, or its printing, before you show a price.
printingUncertain:
type: boolean
description: True when the card is right but its printing (colour, parallel) is not certain. The alternatives usually hold the other printings.
reason:
type: string
enum:
- low_confidence
- no_card
- name_not_on_card
description: 'Why `match` is null: `low_confidence` (no card scored 0.75 or more), `no_card` (no card in the photo), `name_not_on_card` (the closest card''s name is not printed on the card in the photo). The alternatives still come back.'
nameCheck:
type: string
enum:
- match
- switched
- not_on_card
- no_match
- no_text
- skipped
- unavailable
description: 'On a match under 0.85: how the name printed on the card compared with the pick.'
usage:
type: object
properties:
scans:
type: object
properties:
used:
type:
- integer
- 'null'
description: Scans used this month, after this one.
allowance:
type: integer
required:
- match
- alternatives
- engine
additionalProperties: true
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
Retry-After:
description: Seconds to wait before retrying (busy scanner, movers rebuilding).
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
RateLimit-Limit:
description: Requests allowed in the current one-second window (X-RateLimit-* are sent too).
schema:
type: integer
X-HCI-Plan:
description: 'Commercial plans only: the plan id behind this key.'
schema:
type: string
responses:
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