Alpha Release: JK Index is in active development. Data coverage is expanding and you may see gaps or inconsistencies as we iterate. Feedback is welcome.

JK Index TCG Pricing API v0 — Public Contract (Private Beta)

API Basehttps://api.jkindex.io
Status

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/coverage with 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-Id in 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 the segment query parameter to receive non-null pricing.
  • If segment is omitted: Identity may match; pricing is null; pricing.reason_code="pricing_requires_segment" (fail-closed).
  • If segment is 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"
Having trouble? Check /status and include X-Request-Id in support DMs.

5. Endpoints

5.1 GET /api/v0/price

Query params

ParamRequiredDescription
tcgyesTCG slug (e.g. pokemon). Case-insensitive.
setyesSet slug (e.g. jungle) or set_code (e.g. JNG). Case-insensitive.
numberyesCard number in a supported format (54, #54, 54/102, etc.). See §6.3.
variantnoDisambiguation (e.g. 1st, unlimited). See §6.4.
segmentnoOptional 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.
languagenoDefault en. [WARN] Accepted but may not be enforced yet.
currencynousd (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_code explains.

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&currency=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&currency=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&currency=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&currency=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&currency=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

  • tcg is 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 canonical set_code from DB.

6.3 Number formats in-scope

Supported:

Input styleExamplesNotes
Numeric54, 054Leading zeros normalized.
# prefix#54, # 54Single optional # stripped.
Fraction54/102Numerator used for lookup.
Alpha suffix54aPass-through if in index.
Promo/setSWSH020, ST01-001Alnum + hyphens.
SubsetTG12, GG35If 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 1st does not match request 1ST).
  • Variant matches: card_metadata.variant when 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_codeMeaningSuggested client action
unsupported_tcgTCG not in index.Check coverage; use valid tcg slug.
unsupported_setSet not in index for this TCG.Check coverage; use valid set slug or set_code.
invalid_number_formatNumber empty or structurally invalid.Send non-empty, valid number format.
unsupported_number_formatNumber format not in scope (§6.3).Use a supported format or surface "format not supported".
card_not_in_setNo card in set for this number.Treat as no listing; do not retry same input.
variant_not_foundSet+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_languageLanguage not supported.Use en or supported language.
unsupported_segmentRequested 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_unavailableCAD 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_codeMeaning
pricing_requires_segmentClient did not send segment; pricing not returned (fail-closed).
no_pricing_for_segmentSegment supported but no data for this card/segment.
fx_unavailablecurrency=cad requested but FX rate missing; pricing not returned in CAD.

8. CAD / FX behavior

  • [OK] currency=cad converts from stored USD using cached FX (USD/CAD).
  • [OK] When CAD is returned in pricing.snapshot, response includes fx_rate, fx_as_of, fx_source inside the snapshot.
  • [NOTE] fx_rate is 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 returns public, 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). When pricing.snapshot is null, as_of is 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&currency=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&currency=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&currency=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&currency=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&currency=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&currency=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.