Skip to content
Prepaid, no subscription trap. Top up a balance and we draw the monthly fee from it.
tcggraph

Credits and rate limits

Credits measure what you use in a billing period; the rate limit caps how fast one key uses it. There is no daily cap. Every response says where you stand.

What each call costs

Calls are priced by what they are, not by how many rows they return: a page of 100 cards costs the same 2 credits as a page of one. Each endpoint’s page states its cost, and X-Credits-Cost on every 2xx response says what that call was charged.

OperationNotesCredits
Card lookup by idGET /v1/cards/{id}, any game1
Current prices for one cardGET /v1/prices/{cardId}: every source, graded and per language1
Search or filterGET /v1/cards, or a set with its cards; per page of up to 1002
GraphQL requestOne request, whatever it selects; pages cap at 100 cards2
Price history or salesOne card's series, up to five years per call3
Games and set listsGET /v1/games, /v1/games/{slug}, /v1/sets1
Image deliveryNever metered, on any planFree
Cached response (304)Send If-None-Match and pay nothingFree
ErrorsA 4xx or 5xx is never billedFree
Webhooks and price watchesManaging them and every delivery; watches are capped per plan insteadFree

A GraphQL request is 2 credits however many root fields it has; a document that fails validation is not billed. Card recognition is metered in scans, not credits, on Card recognition.

Per-plan allowances

PlanCredits / periodRequests / minuteOverage
Starter10,00060 req/min$5 per 1,000
Growth100,000300 req/min$1.50 per 1,000
Scale500,0001,000 req/min$0.80 per 1,000

The period is your billing month, counted from the day the plan started, not the calendar month. A yearly plan draws twelve months’ credits over its year. The rate limit is per key, counted in clock minutes; every request with a valid key counts, including free ones and ones that fail. There is no daily cap: spend your credits at whatever pace you need. Past the allowance, requests keep succeeding as overage, drawn from your prepaid balance at a higher rate than the plan's own. If you are paying overage two months running, the next plan up is cheaper. Set a hard cap if you would rather stop at the allowance.

Response headers

On every metered response. The same headers are exposed to browser JavaScript through CORS.

X-Credits-Cost: 2            # what this call was charged
X-Credits-Limit: 150000      # the billing period's allowance
X-Credits-Used: 48213        # used this period, this call included
X-Credits-Remaining: 101787  # allowance left
X-Daily-Limit: unlimited     # there is no daily cap; kept for older clients
X-RateLimit-Limit: 300       # requests per minute for this key
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1790190420   # unix seconds: the next minute boundary
X-Credits-Overage: 0         # only once past the allowance: overage credits this period

When a limit is reached

WhatStatusdetails.limitUntil
Requests per minute, per key429 rate_limitedrate_limitThe next minute. Retry-After: 60; X-RateLimit-Reset is the exact second.
Monthly allowance, with a hard cap set429 rate_limitedmonthly_allowanceThe next billing period, or until you raise the plan or turn the hard cap off. details.hardCap is true.
Monthly allowance, without a hard cap200—Calls keep working and overage is drawn from your balance. If the balance cannot cover it: 402 balance_empty, with details.balanceCents and details.requiredCents, until you top up.

The per-minute limit:

429
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit of 300 requests per minute exceeded on the Growth plan.",
    "details": { "limit": "rate_limit", "plans": "https://tcggraph.com/pricing" }
  }
}

Only rate_limit clears within a minute, so only it is worth retrying automatically:

async function call(url, init) {
  for (let attempt = 0; ; attempt++) {
    const response = await fetch(url, init);
    if (response.status !== 429 || attempt === 3) return response;
    const body = await response.clone().json();
    // A hard monthly cap will not lift in seconds: stop, do not spin.
    if (body.error.details?.limit !== "rate_limit") return response;
    const wait = Number(response.headers.get("Retry-After") ?? 60);
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

What is never billed

  • Card images from the CDN, on every plan.
  • A 304: send the ETag back as If-None-Match and an unchanged response costs nothing.
  • Any error, 4xx or 5xx, including the limit responses above.
  • Webhook and price-watch management, and every webhook delivery.

Plans and the hard cap are on pricing and in the dashboard.