One API for eight card games.

Card search, multi-marketplace prices, and a fair-value band with a confidence score, over plain REST and JSON. Every call is keyed, so your quota is yours.

Pre-launch

The API is running and rate limited per key today. What is not ready is the public hostname and self-serve key issuance. Paths, parameters and payloads on this page match the shipped code. Only https://api.cardintel.gg is a placeholder.

Quickstart

Three steps, one header, no SDK.

1. Get a key

Self-serve signup is not live yet. Ask and we provision a free-tier key: same key format and same limits as the eventual dashboard will issue.

Get an API key
2. Make your first request

Every request carries the key in the X-Card-Intel-Api-Key header.

shell
export CARDINTEL_KEY="ci_live_..."

curl -H "X-Card-Intel-Api-Key: $CARDINTEL_KEY" \
  "https://api.cardintel.gg/v2/cards?game=pokemon&q=charizard&limit=20"
3. Read the response

Search returns a paged envelope: data, total, limit, offset. Page with offset until offset + limit >= total.

200 OK
{
  "data": [
    {
      "id": "3f7c1b90-2ad5-4f0e-9c2b-8ad1b70f4e11",
      "game": "pokemon", "name": "Charizard VMAX",
      "set_code": "swsh4", "set_name": null,
      "number": "020/185", "rarity": "Rare Holo VMAX",
      "image_url": "https://images.pokemontcg.io/swsh4/020_hires.png",
      "prices": null, "trend_30d_pct": null,
      "last_updated": "2026-07-24T09:12:44+00:00"
    }
  ],
  "total": 37, "limit": 20, "offset": 0
}

Note prices: null. Search is a lookup, not a pricing call. Take the id to /v2/cards/{card_id} for the priced payload.

Endpoint reference

Eight routes. All GET, all JSON, all keyed except /v2/health.

RouteReturns
GET /v2/gamesEvery supported game with its card and price-observation counts.
GET /v2/cardsSearch cards across all games. Substring match on card name and description.q, game, set_code, rarity, limit (1-100, default 20), offset
GET /v2/cards/{card_id}One card with multi-source price aggregation, fair value, and its 30-day trend.
GET /v2/cards/{card_id}/pricesRaw price observations as a time series. One row per observed sale or listing.window_days (7-730, default 90), source, limit (1-5000, default 500)
GET /v2/games/{game}/setsSet codes for one game, ordered by card count.
GET /v2/market/moversTop gainers or losers by 30-day median trend.game, direction (gainers | losers, default gainers), limit (1-100, default 20)
GET /v2/meIntrospect the presented key: tier, enforced rate limits, subscription state.
GET /v2/healthRow counts, game count, and per-source ingest freshness. The only route that takes no key.

The generated OpenAPI 3.1 schema is served at /openapi.json and browsable at /docs.

Response shapes

Any source with nothing to say returns null instead of a guess.

GET /v2/cards/{card_id}

The priced payload. tcgplayer and cardmarket are per-venue aggregates over the trailing 30 days; cardintel_fair_value is the 90-day statistical band with its confidence score.

200 OK
{
  "id": "3f7c1b90-2ad5-4f0e-9c2b-8ad1b70f4e11",
  "game": "pokemon", "name": "Charizard VMAX",
  "set_code": "swsh4", "set_name": null,
  "number": "020/185", "rarity": "Rare Holo VMAX",
  "prices": {
    "tcgplayer": {
      "median_usd": 28.5, "low_usd": 22.0, "high_usd": 45.0,
      "observation_count": 31, "currency": "USD", "market": 28.5
    },
    "cardmarket": null,
    "cardintel_fair_value": {
      "median_usd": 26.8, "p25_usd": 23.5, "p75_usd": 31.0,
      "confidence": 0.85, "observation_count": 47, "window_days": 90
    },
    "observation_count": 31, "last_updated": null
  },
  "trend_30d_pct": -3.2,
  "last_updated": "2026-07-24T09:12:44+00:00"
}

GET /v2/me

The limits reported here are read from the same provider the limiter enforces, so they cannot drift from the numbers actually applied to your key.

200 OK
{
  "tier": "public",
  "key_source": "db",
  "key_id": "9c1e4d02-7b31-4a55-9f0a-1d2c3b4e5f60",
  "label": "my-first-app",
  "subscription_status": "active",
  "subscription_id": "sub_01j9x2m4",
  "current_period_end": "2026-08-24T00:00:00",
  "limits": {
    "rate_limit": "60/minute;1000/day",
    "requests_per_minute": 60,
    "requests_per_day": 1000
  }
}

Rate limits

One bucket for the whole /v2 surface, metered per key.

TierRequests / minRequests / day
Freeany valid key601,000
Dealerpaid600unmetered
Operatorinternal6,000unmetered
One bucket for all of /v2, per key
The 60/min and 1,000/day windows are a single shared budget across every keyed route, not 60/min per endpoint. Both windows are enforced independently; whichever empties first produces the 429.
Metered per key, not per IP
Any recognised key gets its own bucket, free tier included, so you are never sharing an allowance with whoever else is behind your NAT.
/v2/health is outside the budget
The open health route is throttled per IP at 30/min instead, so an uptime probe can never drain a customer's quota.

Errors

JSON with a detail field: a string for auth failures, an object with a stable error code for the rest.

401

No X-Card-Intel-Api-Key header on any route except /v2/health.

{ "detail": "api_key_required" }
401

Header present but the key matched nothing: unknown, revoked, or lapsed subscription.

{ "detail": "api_key_invalid" }
404

Card id not found.

{ "detail": { "error": "card_not_found", "card_id": "..." } }
429

Either the per-minute or the daily window is exhausted for your key.

{ "detail": { "error": "rate_limit_exceeded", "limit": "60 per 1 minute" } }

A missing header and an unrecognised key both return 401 by design: a probe should not be able to tell “no keys configured” from “wrong key”.

What is not true yet

The gaps between the pitch and the running code, in full. This list shrinks. It never quietly disappears.

The base URL is a placeholder
https://api.cardintel.gg is not resolvable yet. The public hostname ships with the launch; today the API answers on whatever host you run it on, and the local default is http://127.0.0.1:18100. Every path and payload on this page is real. Only the origin is pending.
Auth is a custom header, not Bearer
Send X-Card-Intel-Api-Key: <key>. There is no Authorization: Bearer form and no ?api_key= query parameter; anything that says otherwise is out of date. Every /v2 route except /v2/health requires a key, free tier included. Free, but never anonymous, so usage meters per key instead of per IP.
Self-serve key issuance is not live
Keys exist and are honoured today, but they are issued by hand. Ask for one and we provision it. The dashboard that issues them yourself is next.
Search results carry no prices
/v2/cards returns prices: null on every row. The aggregation is per card and too expensive to run across a result page. Fetch /v2/cards/{card_id} for the priced payload.
set_name is always null
Set metadata is keyed by code today. set_code is populated and stable; set_name is reserved and returns null on every route that exposes it, including /v2/games/{game}/sets.
No X-RateLimit-* response headers yet
Limits are enforced but not advertised per response. Read your allowance from /v2/me and back off on 429. Buckets are held in process memory, so a deploy resets them.
Fair value refuses to guess
cardintel_fair_value needs at least 3 observations in the window. Below that it comes back null rather than a fabricated number. Sparse data is reported, never smoothed over.