Private known-card beta · USD only
JK Index Pricing API v0 consumer documentation
This is the integration source of truth for all four supported V0 routes. It covers resolution, ambiguity selection, price snapshots, batch, history, caching, authentication, quotas, and errors.
If you see 404 on /api/v0/*, you're likely calling jkindex.io instead of api.jkindex.io.
1. Base URL and authentication
Send every request to https://api.jkindex.io. The website host does not serve the API. Every supported V0 route requires a hand-issued, per-consumer bearer key:
Authorization: Bearer YOUR_API_KEY- Never put a key in a query string, browser URL, repository, or support message.
- A revoked, inactive, missing, or invalid key returns the same stable 401 body.
- Keys and both quotas are per consumer. The exact per-minute and rolling-30-day limits are communicated at issuance.
- Every response carries
X-Request-Id. Include it with the UTC timestamp when reporting an issue.
2. First known-card request
V0 is structured resolution, not search. Start with a TCG identifier, set identifier, and collector number that you already hold.
curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "tcg=pokemon" \
--data-urlencode "set=jungle" \
--data-urlencode "number=64" \
--data-urlencode "segment=raw_nm" \
--data-urlencode "currency=usd" \
"https://api.jkindex.io/api/v0/price"{
"outcome": "matched",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"image_url_absolute": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"display_name": "Pikachu",
"edition": "1st",
"match": {
"status": "exact",
"confidence": "exact",
"reason_codes": [
"set_slug_exact",
"collector_number_exact"
]
},
"confidence": "exact",
"candidate_ref": "c1_<opaque-reference-from-response>",
"other_printings": [],
"pricing": {
"segment": "raw_nm",
"as_of": "2026-07-30T18:00:00Z",
"snapshot": {
"latest": "30.00",
"median": "20.00",
"count": 3,
"currency": "USD"
},
"freshness": {
"state": "fresh",
"age_days": 1,
"fresh_max_age_days": 7,
"stale_after_days": 21
},
"segment_basis": "strict_segment",
"reason_code": null
}
}Money is serialized as fixed-decimal strings. canonical_id is a stable identity value, not a selection protocol.
3. Understand matched, ambiguous, and not-found outcomes
| Outcome | Meaning | Next action |
|---|---|---|
matched | Exactly one printing resolved. | Read identity and the segment-scoped pricing block. |
ambiguous | Several active printings match. | Choose a candidate and copy its candidate_ref verbatim. |
not_found | The reference or requested contract dimension failed closed. | Branch on the top-level reason_code; do not infer catalog-wide absence. |
These are domain answers over HTTP 200. Only a matched response has match, categorical confidence, and pricing. Ambiguous and not-found outcomes do not carry a top-level confidence value.
{
"outcome": "ambiguous",
"candidates": [
{
"canonical_id": "jungle-054-jigglypuff-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "54",
"number_display": "54",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": null,
"display_name": "Jigglypuff",
"edition": "1st",
"variant": "1st",
"candidate_ref": "c1_<first-opaque-reference>",
"variant_is_unique": false,
"image_url_absolute": null
},
{
"canonical_id": "jungle-054-jigglypuff-unlimited",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "54",
"number_display": "54",
"variant": "unlimited",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": null,
"display_name": "Jigglypuff",
"edition": "unlimited",
"variant": "unlimited",
"candidate_ref": "c1_<second-opaque-reference>",
"variant_is_unique": true,
"image_url_absolute": null
}
]
}{
"outcome": "not_found",
"reason_code": "unknown_candidate_ref",
"message": "unknown_candidate_ref"
}4. Select an ambiguous printing with candidate_ref
- Render candidate identity, display name, edition, variant, and image as provided.
- Keep the selected candidate's opaque
candidate_refunchanged. - Repeat price or history with that value. Do not parse or construct a reference.
CANDIDATE_REF='PASTE_CANDIDATE_REF_VALUE_VERBATIM'
curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "tcg=pokemon" \
--data-urlencode "set=jungle" \
--data-urlencode "number=54" \
--data-urlencode "candidate_ref=${CANDIDATE_REF}" \
--data-urlencode "segment=raw_nm" \
"https://api.jkindex.io/api/v0/price"{
"outcome": "matched",
"canonical_id": "jungle-054-jigglypuff-unlimited",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "54",
"number_display": "54",
"variant": "unlimited",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": null,
"image_url_absolute": null,
"display_name": "Jigglypuff",
"edition": "unlimited",
"match": {
"status": "exact",
"confidence": "exact",
"reason_codes": [
"set_slug_exact",
"candidate_ref_match"
]
},
"confidence": "exact",
"candidate_ref": "c1_<second-opaque-reference>",
"other_printings": [],
"pricing": {
"segment": "raw_nm",
"as_of": "2026-07-30T18:00:00Z",
"snapshot": {
"latest": "30.00",
"median": "20.00",
"count": 3,
"currency": "USD"
},
"freshness": {
"state": "fresh",
"age_days": 1,
"fresh_max_age_days": 7,
"stale_after_days": 21
},
"segment_basis": "strict_segment",
"reason_code": null
}
}A recognized reference selects exactly one active card in the supplied TCG and set. Empty whitespace is treated as no reference. Every other supplied value— malformed, stale after catalog changes, or unknown—returns unknown_candidate_ref and never falls back to number matching. Candidate refs have no published expiration time.
variant remains exact-after-trim and case-sensitive. It can help, but candidates with variant_is_unique: false prove that a variant is not always sufficient; candidate_ref is the reliable path.
5. GET /api/v0/price
| Parameter | Required | Contract |
|---|---|---|
tcg | yes | pokemon or one-piece; case-insensitive. |
set | yes | Set slug, set code, or registered alias; case-insensitive. |
number | yes | Structured collector number; normalization is listed below. |
variant | no | Exact-after-trim, case-sensitive printing discriminator. |
language | no | Defaults to en; accepted and echoed, not an enforced catalog filter. |
segment | no | Required for non-null pricing; one of the seven supported segments. |
currency | no | Defaults to usd; USD is the only supported currency. |
candidate_ref | no | Opaque selector copied verbatim from a prior candidate or match. |
Supported number forms
Numeric (54, 054), optional hash (#54), fraction (54/102), alpha suffix (54a), subset code (TG12), and alphanumeric prefixed forms (OP01-121) are supported. Prefix matching is set-scoped; the resolver never guesses across sets.
Verified One Piece forms include set alias OP01 withOP01-121 or bare 121, case-insensitiveop01-001, and set OP07 with eitherOP07-119 or bare 119. The OP01-121 production shape is legitimately ambiguous and returns candidate refs.
curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "tcg=one-piece" \
--data-urlencode "set=OP01" \
--data-urlencode "number=OP01-121" \
"https://api.jkindex.io/api/v0/price"
# The verified bare-number equivalent uses set=OP01 and number=121.Pricing-null cases
- No segment: matched identity with
pricing.reason_code: pricing_requires_segment. - Supported segment with no qualifying sale: matched identity with
pricing.reason_code: no_pricing_for_segment. - Unsupported segment: top-level not-found
unsupported_segmentplussupported_segments. - Non-USD: top-level not-found
unsupported_currencyplussupported_currencies: ["USD"].
{
"outcome": "matched",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"image_url_absolute": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"display_name": "Pikachu",
"edition": "1st",
"match": {
"status": "exact",
"confidence": "exact",
"reason_codes": [
"set_slug_exact",
"collector_number_exact"
]
},
"confidence": "exact",
"candidate_ref": "c1_<opaque-reference-from-response>",
"other_printings": [],
"pricing": {
"segment": "psa_10",
"snapshot": null,
"reason_code": "no_pricing_for_segment"
}
}6. POST /api/v0/price/batch
Batch is live. Send references with 1–50 items. Each item accepts the same dimensions as price plus optional request_id(maximum 128 characters, echoed) and per-item candidate_ref.
- Input order and count are preserved; results carry zero-based
index. - Each item resolves independently. A domain-invalid item is a per-item outcome; it does not fail valid siblings.
- A structurally invalid body or item fails the whole request with 422.
- More than 50 references fails with 400
batch_too_largebefore lookup. - Quota cost is N references, reserved all-or-nothing before resolution. A refused batch consumes nothing.
- Identical references are resolved once and fanned out; each item still appears in its original position.
{
"references": [
{
"request_id": "direct-match",
"tcg": "pokemon",
"set": "jungle",
"number": "64",
"segment": "raw_nm",
"currency": "usd"
},
{
"request_id": "needs-selection",
"tcg": "pokemon",
"set": "jungle",
"number": "54"
},
{
"request_id": "bad-reference",
"tcg": "pokemon",
"set": "jungle",
"number": "54",
"candidate_ref": "c1_<unknown-opaque-reference>"
}
]
}{
"outcome": "batch",
"count": 3,
"generated_at": "2026-07-31T18:00:00Z",
"results": [
{
"index": 0,
"request_id": "direct-match",
"etag": "\"0123456789abcdef\"",
"outcome": "matched",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"image_url_absolute": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"display_name": "Pikachu",
"edition": "1st",
"match": {
"status": "exact",
"confidence": "exact",
"reason_codes": [
"set_slug_exact",
"collector_number_exact"
]
},
"confidence": "exact",
"candidate_ref": "c1_<opaque-reference-from-response>",
"other_printings": [],
"pricing": {
"segment": "raw_nm",
"as_of": "2026-07-30T18:00:00Z",
"snapshot": {
"latest": "30.00",
"median": "20.00",
"count": 3,
"currency": "USD"
},
"freshness": {
"state": "fresh",
"age_days": 1,
"fresh_max_age_days": 7,
"stale_after_days": 21
},
"segment_basis": "strict_segment",
"reason_code": null
}
},
{
"index": 1,
"request_id": "needs-selection",
"etag": "\"123456789abcdef0\"",
"outcome": "ambiguous",
"candidates": [
{
"canonical_id": "jungle-054-jigglypuff-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "54",
"number_display": "54",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": null,
"display_name": "Jigglypuff",
"edition": "1st",
"variant": "1st",
"candidate_ref": "c1_<first-opaque-reference>",
"variant_is_unique": false,
"image_url_absolute": null
},
{
"canonical_id": "jungle-054-jigglypuff-unlimited",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "54",
"number_display": "54",
"variant": "unlimited",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": null,
"display_name": "Jigglypuff",
"edition": "unlimited",
"variant": "unlimited",
"candidate_ref": "c1_<second-opaque-reference>",
"variant_is_unique": true,
"image_url_absolute": null
}
]
},
{
"index": 2,
"request_id": "bad-reference",
"etag": "\"23456789abcdef01\"",
"outcome": "not_found",
"reason_code": "unknown_candidate_ref",
"message": "unknown_candidate_ref"
}
]
}Each result is a normal price outcome plus index,request_id, and its quoted etag. The batch-levelgenerated_at is assembly time; price time remainspricing.as_of per item.
7. GET /api/v0/price/history
History returns derived aggregates, never raw sale rows. Identity must resolve to one card and segment is required.
| Parameter | Required/default | Contract |
|---|---|---|
tcg, set, number | required | Same resolution rules as price. |
segment | required | One supported raw or PSA segment. |
currency | usd | USD only. |
variant, language, candidate_ref | optional | Same identity semantics as price. |
start | end minus 90 days | Inclusive ISO date. |
end | today | Inclusive ISO date; future input clamps to today. |
interval | week | day, week, month. |
limit | 400 | 1–400 points. |
cursor | optional | Exclusive: return buckets after this prior interval_start. |
The inclusive range may span at most 366 days. Points are ascending. Missing intervals are omitted, not zero-filled or forward-filled. An empty series returns points: [], point_count: 0,series_freshness: null, and next_cursor: null.
An ambiguous history reference returns the candidates plus message: history_requires_unambiguous_reference. Copy a candidate ref into the same history request to obtain the series.
curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "tcg=pokemon" \
--data-urlencode "set=jungle" \
--data-urlencode "number=64" \
--data-urlencode "segment=raw_nm" \
--data-urlencode "start=2026-06-01" \
--data-urlencode "end=2026-07-31" \
--data-urlencode "interval=week" \
--data-urlencode "limit=2" \
"https://api.jkindex.io/api/v0/price/history"{
"outcome": "history",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"segment": "raw_nm",
"currency": "USD",
"interval": "week",
"start": "2026-06-01",
"end": "2026-07-31",
"points": [
{
"interval_start": "2026-07-06",
"median": "20.00",
"latest": "30.00",
"observation_count": 3
},
{
"interval_start": "2026-07-13",
"median": "22.00",
"latest": "24.00",
"observation_count": 2
}
],
"point_count": 2,
"missing_intervals_policy": "omitted",
"series_freshness": {
"state": "fresh",
"age_days": 1,
"fresh_max_age_days": 7,
"stale_after_days": 21,
"last_sale_at": "2026-07-30T18:00:00Z"
},
"next_cursor": "2026-07-13",
"retention": "full_source_history"
}curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "tcg=pokemon" \
--data-urlencode "set=jungle" \
--data-urlencode "number=64" \
--data-urlencode "segment=raw_nm" \
--data-urlencode "start=2026-06-01" \
--data-urlencode "end=2026-07-31" \
--data-urlencode "interval=week" \
--data-urlencode "limit=2" \
--data-urlencode "cursor=2026-07-13" \
"https://api.jkindex.io/api/v0/price/history"{
"outcome": "history",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"segment": "raw_nm",
"currency": "USD",
"interval": "week",
"start": "2026-06-01",
"end": "2026-07-31",
"points": [
{
"interval_start": "2026-07-20",
"median": "25.00",
"latest": "27.00",
"observation_count": 2
}
],
"point_count": 1,
"missing_intervals_policy": "omitted",
"series_freshness": {
"state": "fresh",
"age_days": 1,
"fresh_max_age_days": 7,
"stale_after_days": 21,
"last_sale_at": "2026-07-30T18:00:00Z"
},
"next_cursor": null,
"retention": "full_source_history"
}8. Handle freshness, unavailable pricing, and images
A priced response carries as_of, a snapshot, andfreshness with state, age_days,fresh_max_age_days, and stale_after_days.fresh is at or below the first threshold,aging is between thresholds, and stale is older. Stale pricing remains served and is labeled; it is never presented as current.
{
"outcome": "matched",
"canonical_id": "jungle-064-pikachu-1st",
"identity": {
"tcg": "pokemon",
"set_slug": "jungle",
"set_code": "JNG",
"number_norm": "64",
"number_display": "64",
"variant": "1st",
"language": "en",
"set_alias": null,
"matched_set_alias": null
},
"image_url": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"image_url_absolute": "https://cdn.jkindex.io/cards/jungle/64-1st.webp",
"display_name": "Pikachu",
"edition": "1st",
"match": {
"status": "exact",
"confidence": "exact",
"reason_codes": [
"set_slug_exact",
"collector_number_exact"
]
},
"confidence": "exact",
"candidate_ref": "c1_<opaque-reference-from-response>",
"other_printings": [],
"pricing": {
"segment": "raw_nm",
"as_of": "2026-05-31T18:00:00Z",
"snapshot": {
"latest": "30.00",
"median": "20.00",
"count": 3,
"currency": "USD"
},
"freshness": {
"state": "stale",
"age_days": 61,
"fresh_max_age_days": 7,
"stale_after_days": 21
},
"segment_basis": "strict_segment",
"reason_code": null
}
}No qualifying observation is different from stale data: no_pricing_for_segment has a null snapshot and noas_of or freshness. History freshness describes the newest sale across the whole requested series, not only the current page.
image_url is the stored value and may be null.image_url_absolute passes an absolute URL through, joins a configured asset base only for supported relative paths, and otherwise remains null. Render these values as returned; never invent a card-image host or path.
9. Cache with ETag and conditional 304
| Route | Cache-Control | If-None-Match |
|---|---|---|
GET /api/v0/price | public, max-age=300 | 304 supported |
POST /api/v0/price/batch | public, max-age=300 | 304 supported |
GET /api/v0/price/history | public, max-age=900 | 304 supported |
GET /api/v0/health | private, max-age=30 | Not supported; always 200 when authenticated |
ETags are quoted opaque values. Cache keys must include every request dimension, including TCG, set form, number form, variant, language, segment, currency, candidate ref, and history page dimensions. Never construct or parse an ETag.
curl --get \
-H "Authorization: Bearer YOUR_API_KEY" \
-H 'If-None-Match: "0123456789abcdef"' \
--data-urlencode "tcg=pokemon" \
--data-urlencode "set=jungle" \
--data-urlencode "number=64" \
--data-urlencode "segment=raw_nm" \
"https://api.jkindex.io/api/v0/price"
# Unchanged representation:
HTTP/1.1 304 Not Modified
ETag: "0123456789abcdef"
Cache-Control: public, max-age=300
X-Request-Id: 12345678-1234-1234-1234-123456789abc10. Handle errors, retries, and rate limits
| HTTP | Meaning | Retry |
|---|---|---|
| 200 | Domain envelope: matched, ambiguous, not_found, batch, history, or health. | Branch on the body; do not retry a deterministic reason unchanged. |
| 304 | Cached price, batch, or history representation is current; empty body. | Use the cached body. |
| 400 | Batch exceeds 50 items. | Split the batch. |
| 401 | Missing, invalid, inactive, or revoked key. | Stop and correct credentials; do not loop. |
| 422 | FastAPI request validation failed. | Correct the request shape. |
| 429 | Per-minute or rolling-30-day per-key limit refused the request. | Honor Retry-After; refused work consumes nothing. |
| 5xx | Unexpected service failure. | Bounded exponential backoff with jitter; include request ID if persistent. |
The supported V0 application does not emit an application-level 403. A 403 from an upstream edge or unusual client is outside the JSON contract; record the body, headers, client, UTC timestamp, and request ID rather than treating it as a bad key.
Stable public headers are X-Request-Id on every response,ETag and Cache-Control on successful domain responses, and Retry-After on 429. There are no promisedX-RateLimit-Remaining headers.
{
"error": "unauthorized",
"message": "Missing or invalid API key"
}{
"detail": [
{
"type": "missing",
"loc": [
"query",
"tcg"
],
"msg": "Field required",
"input": null
}
]
}HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-Request-Id: 12345678-1234-1234-1234-123456789abc
{
"error": "rate_limited",
"message": "Too many requests"
}GET /api/v0/health
This route requires the same bearer key and does not consume the pricing rate limit. It always returns HTTP 200 after authentication with status, UTC time, deployed version, and checks.
{
"status": "ok",
"time": "2026-07-31T18:00:00Z",
"version": "0123456789ab",
"checks": {
"db": "ok",
"fx_table": "ok",
"fx_rate_usd_cad": "ok"
}
}checks.db is relevant to request serviceability. Thefx_table and fx_rate_usd_cad entries are operator diagnostics retained in the health envelope; they do not enable or imply consumer currency conversion. USD remains the only supported currency.
The public website status page calls unauthenticated readiness and checks route presence. It is not the keyed health response and does not spend your key quota.
Coverage and catalog discovery are excluded
GET /api/v0/coverage is not registered, is absent from public OpenAPI, and returns the standard 404 {"detail":"Not Found"}. The beta does not enumerate every supported set or card and does not promise an exact live coverage summary. Consumers provide known identifiers. A per-request not_found or no_pricing_for_segmentresult must not be generalized to the catalog.
Current changelog
- 2026-07-31 — Consumer documentation parity. All four live routes, exact response schemas, bearer security, batch/history workflows, caching, errors, and public examples aligned with runtime.
- 2026-07-31 — Controlled beta without discovery. USD known-card resolution and pricing remain supported; coverage and catalog enumeration are excluded.
- 2026-07-29 — USD beta closure. Non-USD fails closed; candidate refs complete ambiguity selection; unknown refs do not fall back; matched results disclose other printings.
- 2026-07-28 — Consumer-readiness additions. One Piece prefixed/bare-number corrections, variant detail, confidence correction, truthful image URL handling, freshness labels, ETag/304, batch, history, authentication, and quota hardening.
Support checklist
Report the exact request without its key, full response, UTC timestamp, X-Request-Id, and ETag. Load, soak, stress, or high-concurrency testing requires prior written authorization with an explicit request budget, concurrency cap, time window, and stop conditions.
On this page
JK Index was created under Riot Games' "Legal Jibber Jabber" policy using assets owned by Riot Games. Riot Games does not endorse or sponsor this project.
JK Index isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
JK Index is an independent collection and price-tracking platform. Riftbound card images and market data shown on this site come from JK Index’s own independent data sources, not from Riot Games or a Riot API.