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.
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.
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 keyEvery request carries the key in the X-Card-Intel-Api-Key header.
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"Search returns a paged envelope: data, total, limit, offset. Page with offset until offset + limit >= total.
{
"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.
| Route | Returns |
|---|---|
| GET /v2/games | Every supported game with its card and price-observation counts. |
| GET /v2/cards | Search 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}/prices | Raw 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}/sets | Set codes for one game, ordered by card count. |
| GET /v2/market/movers | Top gainers or losers by 30-day median trend.game, direction (gainers | losers, default gainers), limit (1-100, default 20) |
| GET /v2/me | Introspect the presented key: tier, enforced rate limits, subscription state. |
| GET /v2/health | Row 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.
{
"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.
{
"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.
| Tier | Requests / min | Requests / day |
|---|---|---|
| Freeany valid key | 60 | 1,000 |
| Dealerpaid | 600 | unmetered |
| Operatorinternal | 6,000 | unmetered |
- 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.
No X-Card-Intel-Api-Key header on any route except /v2/health.
{ "detail": "api_key_required" }Header present but the key matched nothing: unknown, revoked, or lapsed subscription.
{ "detail": "api_key_invalid" }Card id not found.
{ "detail": { "error": "card_not_found", "card_id": "..." } }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.