JK Index TCG Pricing API v0 — Public Contract (Private Beta)
If you see 404 on /api/v0/*, you're likely calling jkindex.io instead of api.jkindex.io.
Getting started
- Base URL:
https://api.jkindex.io - Auth:
Authorization: Bearer <key> - First call:
GET https://api.jkindex.io/api/v0/coveragewith the header above. - 404 on /api/v0/*? You are likely calling the website host; use the API base above.
- Check /status and include
X-Request-Idin support DMs.
0. Status
- [NOTE] v0 contract is stable; access is private beta (API keys).
- [NOTE] This document defines request/response behavior. If behavior differs, that is a bug.
1. What this API does
- [OK] Normalized card identity +
canonical_id+ image + segment-scoped price snapshot + confidence + timestamps. - [OK] Segment-scoped pricing (raw condition buckets NM/LP/MP; graded PSA 7–10 in v0). Pricing requires explicit
segment; no blended single price across raw+graded. - [WARN] This is not search. Inputs must be structured (tcg, set, number, optional variant).
2. What this API does NOT do (v0)
- Fuzzy search, name-only search
- Realtime ticks
- Bulk batch lookup
- Recommendations / buy-sell signals
- Blended single price across raw+graded — v0 does not guess or blend markets; pricing is segment-scoped (§3).
3. Pricing is segment-scoped
- Raw and graded markets are distinct. v0 does not blend raw and graded pricing. Raw is segmented by condition buckets (NM, LP, MP); graded is segmented by PSA tier (7, 8, 9, 10).
- Pricing requires an explicit
segment. Send thesegmentquery parameter to receive non-null pricing. - If
segmentis omitted: Identity may match; pricing is null;pricing.reason_code="pricing_requires_segment"(fail-closed). - If
segmentis unsupported:outcome=not_found;reason_code="unsupported_segment"(see §7).
4. Authentication (private beta)
- Header:
Authorization: Bearer <api_key> - [WARN] Keys are rate-limited.
Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54"X-Request-Id in support DMs.5. Endpoints
5.1 GET /api/v0/price
Query params
| Param | Required | Description |
|---|---|---|
| tcg | yes | TCG slug (e.g. pokemon). Case-insensitive. |
| set | yes | Set slug (e.g. jungle) or set_code (e.g. JNG). Case-insensitive. |
| number | yes | Card number in a supported format (54, #54, 54/102, etc.). See §6.3. |
| variant | no | Disambiguation (e.g. 1st, unlimited). See §6.4. |
| segment | no | Optional for request; REQUIRED for non-null pricing. If omitted: identity may match but pricing.snapshot and pricing.segment are null with pricing.reason_code="pricing_requires_segment". Allowed (v0): raw_nm, raw_lp, raw_mp, psa_7, psa_8, psa_9, psa_10. Not supported in v0: raw, graded (aggregate segments). Planned: cgc_10, bgs_10; language-specific later. |
| language | no | Default en. [WARN] Accepted but may not be enforced yet. |
| currency | no | usd (default) or cad. CAD uses cached FX (see §8). When currency=cad and FX unavailable: outcome=matched, pricing.snapshot=null, pricing.reason_code="fx_unavailable". No 503 for FX missing. |
Raw bucket semantics. Raw condition segments (raw_nm/raw_lp/raw_mp) are computed only from JK Index's stored raw-condition signals (e.g., raw_condition fields on sales/listings). We do not infer condition from unstructured text or guess. If the requested raw segment has insufficient data, the response is outcome=matched with pricing.snapshot=null and pricing.reason_code=no_pricing_for_segment.
PSA tier semantics. PSA segments (psa_7/psa_8/psa_9/psa_10) are PSA-only: grading_company must be PSA and grade_value must match the tier. We do not map CGC/BGS grades into PSA tiers. Non-PSA graded segments are not supported in v0 (see planned segments).
Outcomes
- matched — Single card resolved; identity + optional pricing block (segment-scoped; see §3).
- ambiguous — Multiple candidates; no pricing. Client must narrow (e.g. variant) and retry.
- not_found — No card, unsupported segment, or FX failure; top-level
reason_codeexplains.
Response: matched (JSON)
Required: outcome, canonical_id, identity or card, image_url, pricing, confidence. Pricing object: pricing.segment (string | null), pricing.snapshot (object | null), pricing.reason_code (string | null; only when snapshot is null), pricing.as_of (ISO8601; present only when pricing.snapshot is present; otherwise omit). Snapshot fields: latest, median (money as strings, not floats), count, currency; when currency=CAD: fx_rate (string, 6 decimal places), fx_as_of, fx_source inside snapshot. Top-level reason_code is used only when outcome=not_found; when outcome=matched but pricing is null, use pricing.reason_code.
Example (matched, segment omitted — fail-closed pricing):
{
"outcome": "matched",
"canonical_id": "jungle-054-jigglypuff-1st",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff"
},
"image_url": "https://jkindex.io/cards/jungle-054.jpg",
"pricing": {
"segment": null,
"snapshot": null,
"reason_code": "pricing_requires_segment"
},
"confidence": null
}Example (matched, segment=raw_nm, USD):
{
"outcome": "matched",
"canonical_id": "jungle-054-jigglypuff-1st",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff"
},
"image_url": "https://jkindex.io/cards/jungle-054.jpg",
"pricing": {
"segment": "raw_nm",
"as_of": "2026-03-03T22:10:00Z",
"snapshot": {
"latest": "123.45",
"median": "120.00",
"count": 37,
"currency": "USD"
}
},
"confidence": "high"
}Example (matched, segment=psa_8, USD):
{
"outcome": "matched",
"canonical_id": "jungle-054-jigglypuff-1st",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff"
},
"image_url": "https://jkindex.io/cards/jungle-054.jpg",
"pricing": {
"segment": "psa_8",
"as_of": "2026-03-03T22:10:00Z",
"snapshot": {
"latest": "85.00",
"median": "82.00",
"count": 15,
"currency": "USD"
}
},
"confidence": "high"
}Example (matched, segment=psa_10, CAD; FX in snapshot):
{
"outcome": "matched",
"canonical_id": "jungle-054-jigglypuff-1st",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff"
},
"image_url": "https://jkindex.io/cards/jungle-054.jpg",
"pricing": {
"segment": "psa_10",
"as_of": "2026-03-03T22:10:00Z",
"snapshot": {
"latest": "165.00",
"median": "162.00",
"count": 12,
"currency": "CAD",
"fx_rate": "1.352742",
"fx_as_of": "2026-03-03T00:00:00Z",
"fx_source": "ecb"
}
},
"confidence": "high"
}Response: ambiguous (JSON)
outcome: "ambiguous", candidates: array of objects, each with canonical_id, card (same shape as matched: tcg, set, number, name, optional variant), image_url. [NOTE] No pricing block in ambiguous responses.
{
"outcome": "ambiguous",
"candidates": [
{
"canonical_id": "jungle-054-jigglypuff-1st",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff",
"variant": "1st"
},
"image_url": "https://jkindex.io/cards/jungle-054-1st.jpg"
},
{
"canonical_id": "jungle-054-jigglypuff-unlimited",
"card": {
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"name": "Jigglypuff",
"variant": "unlimited"
},
"image_url": "https://jkindex.io/cards/jungle-054-unl.jpg"
}
]
}Response: not_found (JSON)
outcome: "not_found"; top-level reason_code and message only (no pricing.reason_code here).
{
"outcome": "not_found",
"reason_code": "unsupported_number_format",
"message": "unsupported_number_format"
}Example (unsupported_segment):
{
"outcome": "not_found",
"reason_code": "unsupported_segment",
"message": "segment=cgc_10 is not supported in v0"
}Try it (templates)
Base URL: https://api.jkindex.io (private beta). Copy and replace YOUR_API_KEY.
A) matched, segment omitted → pricing_requires_segment
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st"B) matched, segment=raw_nm, currency=usd
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st&segment=raw_nm¤cy=usd"B2) matched, segment=psa_8, currency=usd
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=psa_8¤cy=usd"C) matched, segment=psa_10, currency=cad
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=psa_10¤cy=cad"D) not_found unsupported_segment
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=cgc_10"E) matched, segment=psa_10 but no data → no_pricing_for_segment
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st&segment=psa_10¤cy=usd"F) matched, currency=cad but FX unavailable → fx_unavailable
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=raw_nm¤cy=cad"ambiguous (no variant)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54"not_found — unsupported_number_format
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54!!!"coverage
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/coverage"5.2 GET /api/v0/coverage
Returns supported TCGs and sets (DB-derived). May include a refresh-policy label. [NOTE] Coverage is derived from DB only.
Example shape:
{
"tcgs": [
{
"tcg": "pokemon",
"sets": [
{ "set_slug": "jungle", "set_code": "JNG" },
{ "set_slug": "fossil", "set_code": "FO" }
],
"refresh": "daily"
}
]
}6. Input normalization rules (v0)
6.1 TCG
tcgis case-insensitive; normalized to slug. Unsupported TCG →not_found,reason_code=unsupported_tcg.
6.2 Set
- Request accepts slug or set_code (case-insensitive). Unsupported set →
not_found,reason_code=unsupported_set. [NOTE] Response echoes canonicalset_codefrom DB.
6.3 Number formats in-scope
Supported:
| Input style | Examples | Notes |
|---|---|---|
| Numeric | 54, 054 | Leading zeros normalized. |
| # prefix | #54, # 54 | Single optional # stripped. |
| Fraction | 54/102 | Numerator used for lookup. |
| Alpha suffix | 54a | Pass-through if in index. |
| Promo/set | SWSH020, ST01-001 | Alnum + hyphens. |
| Subset | TG12, GG35 | If in index. |
Unsupported:
- Any format not matching the above →
not_found,reason_code=unsupported_number_format. - Structurally invalid (empty, whitespace-only) →
reason_code=invalid_number_format. No silent fallback.
6.4 Variant semantics (IMPORTANT)
- Variant is exact-after-strip, case-sensitive (e.g. stored
1stdoes not match request1ST). - Variant matches:
card_metadata.variantwhen present; else edition (e.g.1st,unlimited). - Multiple candidates, no variant → ambiguous. Variant provided but no match →
not_found,reason_code=variant_not_found.
6.5 Language
Default "en". [WARN] Accepted but may not be enforced until v0 stabilizes.
7. Reason codes
Top-level reason_code is used only when outcome=not_found. When outcome=matched but pricing is null, use pricing.reason_code instead.
7.1 not_found (top-level reason_code)
| reason_code | Meaning | Suggested client action |
|---|---|---|
unsupported_tcg | TCG not in index. | Check coverage; use valid tcg slug. |
unsupported_set | Set not in index for this TCG. | Check coverage; use valid set slug or set_code. |
invalid_number_format | Number empty or structurally invalid. | Send non-empty, valid number format. |
unsupported_number_format | Number format not in scope (§6.3). | Use a supported format or surface "format not supported". |
card_not_in_set | No card in set for this number. | Treat as no listing; do not retry same input. |
variant_not_found | Set+number match but no card with this variant. | Suggest valid variants from coverage or ambiguous response. |
ambiguous_requires_variant | [NOTE] Reserved. | Send variant param or show candidate list. |
unsupported_language | Language not supported. | Use en or supported language. |
unsupported_segment | Requested segment (e.g. cgc_10) is not supported in v0. | Use a v0 allowed segment: raw_nm, raw_lp, raw_mp, psa_7, psa_8, psa_9, psa_10. |
fx_unavailable | CAD requested but no FX rate available. | Retry later or request USD. See §8. |
7.2 matched with null pricing (pricing.reason_code)
When outcome=matched but pricing.snapshot is null, the server sets pricing.reason_code:
| pricing.reason_code | Meaning |
|---|---|
pricing_requires_segment | Client did not send segment; pricing not returned (fail-closed). |
no_pricing_for_segment | Segment supported but no data for this card/segment. |
fx_unavailable | currency=cad requested but FX rate missing; pricing not returned in CAD. |
8. CAD / FX behavior
- [OK]
currency=cadconverts from stored USD using cached FX (USD/CAD). - [OK] When CAD is returned in
pricing.snapshot, response includesfx_rate,fx_as_of,fx_sourceinside the snapshot. - [NOTE]
fx_rateis returned as a string decimal (not float) to preserve precision. - [ISSUE] If FX is unavailable → no silent fallback to USD.
When client requests currency=cad and no USD/CAD rate is available, the server returns outcome=matched with pricing.snapshot=null and pricing.reason_code="fx_unavailable". Identity and card data are still returned; only the pricing block indicates FX unavailability. Clients must not assume fallback to USD. [NOTE] HTTP 503 is reserved for general server unavailability, not for missing FX data.
9. Caching
- Cache-Control: GET /api/v0/price returns
public, max-age=300(5 min). GET /api/v0/coverage returnspublic, max-age=3600(1 hour). - Cache key MUST include
segment+currency. Otherwise responses can leak (e.g. USD into CAD, or raw into PSA 10). - ETag: Response includes
ETag(quoted string). Whenpricing.snapshotis null,as_ofis omitted; ETag varies by snapshot presence. - If-None-Match: Send
If-None-Match: <etag>; server returns 304 when unchanged.
Example request:
GET /api/v0/price?tcg=pokemon&set=jungle&number=54&segment=raw_nm¤cy=usd
If-None-Match: "jungle-054-jigglypuff-1st-raw_nm-usd-20260301T120000Z"10. Rate limits
- 429 when rate limit exceeded.
- Response shape: JSON body with
error,message; Retry-After header (seconds). - Do not retry on 401/403; respect Retry-After on 429. Use exponential backoff on repeated 429.
Example 429 response:
{
"error": "rate_limited",
"message": "Too many requests"
}Headers (example):
HTTP/1.1 429 Too Many Requests
Retry-After: 60[NOTE] Exact limits and per-key semantics may vary.
11. Contract revisions (private beta)
- v0 contract stability: Request/response shapes and reason codes in this document are stable for v0. Additive changes may occur with notice.
- Breaking changes: Will not be introduced in v0 without a new API version (e.g. v1) or explicit deprecation and timeline.
- Changelog: Significant contract or behavior changes will be documented.
- Draft revisions: 2026-03-03 — Segment-scoped pricing; cache key includes segment+currency.
12. Examples (copy/paste)
Pricing: A) segment omitted → pricing_requires_segment; B) raw_nm USD; B2) psa_8 USD; C) psa_10 CAD; D) not_found unsupported_segment; E) no_pricing_for_segment; F) fx_unavailable. Plus ambiguous, not_found, coverage.
A) Matched identity but missing segment (fail-closed pricing)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st"Expected: outcome=matched, pricing.segment=null, pricing.snapshot=null, pricing.reason_code=pricing_requires_segment.
B) Matched raw_nm pricing (USD)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st&segment=raw_nm¤cy=usd"Expected: outcome=matched, pricing.segment=raw_nm, pricing.snapshot with latest/median/count/currency.
B2) Matched psa_8 pricing (USD)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=psa_8¤cy=usd"Expected: outcome=matched, pricing.segment=psa_8, pricing.snapshot with latest/median/count/currency.
C) Matched psa_10 pricing (CAD)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=psa_10¤cy=cad"Expected: outcome=matched, pricing.segment=psa_10, pricing.snapshot with currency=CAD and fx_rate/fx_as_of/fx_source when FX available.
D) not_found — unsupported_segment
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=cgc_10"Expected: outcome=not_found, reason_code=unsupported_segment.
E) Matched identity, supported segment but no pricing data (no_pricing_for_segment)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=1st&segment=psa_10¤cy=usd"Expected: outcome=matched, pricing.segment=psa_10, pricing.snapshot=null, pricing.reason_code=no_pricing_for_segment.
F) Matched identity, CAD requested but FX unavailable (fx_unavailable)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&segment=raw_nm¤cy=cad"When no USD/CAD rate: outcome=matched, pricing.snapshot=null, pricing.reason_code=fx_unavailable.
Ambiguous (no variant)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54"not_found — unsupported_number_format
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54!!!"Expected: outcome=not_found, reason_code=unsupported_number_format.
not_found — variant_not_found
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/price?tcg=pokemon&set=jungle&number=54&variant=nonexistent"Expected: outcome=not_found, reason_code=variant_not_found.
Coverage
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.jkindex.io/api/v0/coverage"Request beta access or return to the API overview.
On this page
- Status / Health
Live readiness checks and request IDs for debugging.
- Getting started
- Status
- What it does
- What it does NOT do
- Pricing is segment-scoped
- Authentication
- Endpoints
- Input normalization
- Reason codes
- CAD / FX
- Caching
- Rate limits
- Versioning
- Examples