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

Errors

Every error has a stable code to switch on and says what to change. None is billed.

Error shape

Every error response is application/json with one error object. Switch on code, which does not change. message is a sentence for people and may be reworded. details is present when there is something to act on: the field at fault, the values that would have worked, when a limit resets. Two responses have no body: a 405, for a method an existing path does not accept (POST /v1/cards), and a 304, which is not an error.

`q` instead of `name`

curl "https://api.tcggraph.com/v1/cards?q=charizard" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY"
400
{
  "error": {
    "code": "invalid_request",
    "message": "Unknown filter \"q\". Did you mean name=?",
    "details": {
      "field": "q",
      "suggestion": "name",
      "allowed": [
        "page",
        "limit",
        "sort",
        "game",
        "name",
        "text",
        "rarity",
        "set",
        "collectorNumber",
        "artist",
        "language",
        "minPrice",
        "maxPrice",
        "source",
        "region",
        "grader",
        "grade",
        "supertype",
        "subtypes",
        "hp",
        "types",
        "evolvesFrom",
        "attacks",
        "abilities",
        "weaknesses",
        "resistances",
        "retreatCost",
        "convertedRetreatCost",
        "regulationMark",
        "nationalPokedexNumbers",
        "manaCost",
        "manaValue",
        "colors",
        "colorIdentity",
        "typeLine",
        "power",
        "toughness",
        "loyalty",
        "keywords",
        "layout",
        "finishes",
        "cardType",
        "cost",
        "counter",
        "life",
        "attribute",
        "trigger",
        "frameType",
        "race",
        "atk",
        "def",
        "level",
        "linkRating",
        "linkMarkers",
        "pendulumScale",
        "archetypes",
        "banlist",
        "inkCost",
        "inkable",
        "ink",
        "lore",
        "strength",
        "willpower",
        "classifications",
        "moveCost",
        "version",
        "aspects",
        "arena",
        "traits",
        "unique",
        "variant",
        "playCost",
        "dp",
        "digivolveCosts",
        "form",
        "inheritedEffect",
        "securityEffect",
        "memoryCost",
        "reserveCost",
        "durability",
        "elements",
        "classes",
        "speed",
        "line",
        "specifiedCost",
        "comboPower",
        "comboEnergy",
        "specialTraits",
        "characters",
        "era",
        "series",
        "back",
        "minHp",
        "maxHp",
        "minConvertedRetreatCost",
        "maxConvertedRetreatCost",
        "minManaValue",
        "maxManaValue",
        "minCost",
        "maxCost",
        "minPower",
        "maxPower",
        "minCounter",
        "maxCounter",
        "minLife",
        "maxLife",
        "minAtk",
        "maxAtk",
        "minDef",
        "maxDef",
        "minLevel",
        "maxLevel",
        "minLinkRating",
        "maxLinkRating",
        "minPendulumScale",
        "maxPendulumScale",
        "minInkCost",
        "maxInkCost",
        "minLore",
        "maxLore",
        "minStrength",
        "maxStrength",
        "minWillpower",
        "maxWillpower",
        "minMoveCost",
        "maxMoveCost",
        "minCost",
        "maxCost",
        "minPower",
        "maxPower",
        "minHp",
        "maxHp",
        "minLevel",
        "maxLevel",
        "minPlayCost",
        "maxPlayCost",
        "minDp",
        "maxDp",
        "minMemoryCost",
        "maxMemoryCost",
        "minReserveCost",
        "maxReserveCost",
        "minLevel",
        "maxLevel",
        "minPower",
        "maxPower",
        "minLife",
        "maxLife",
        "minDurability",
        "maxDurability",
        "minCost",
        "maxCost",
        "minSpecifiedCost",
        "maxSpecifiedCost",
        "minPower",
        "maxPower",
        "minComboPower",
        "maxComboPower",
        "minComboEnergy",
        "maxComboEnergy"
      ],
      "docs": "https://tcggraph.com/docs/rest#filtering"
    }
  }
}

Status codes

StatusCodeMeaning
400invalid_requestA parameter, filter or body field is unknown, malformed or out of range, or a set code needs ?game= to be unambiguous. details.field names it, details.allowed lists the valid values, and details.suggestion is set when the intended name is obvious.
401unauthenticatedNo key was sent, or the key is not valid or has been revoked.
402no_active_planThe account has no plan. details.plans links to the plans.
402balance_emptyPast the monthly allowance, and the balance will not cover the overage. details.balanceCents and details.requiredCents give the numbers.
403forbiddenThe account is suspended; the key was used from an origin not on its allowed list; or creating a watch would exceed the plan's price-watch allowance (details.limit is price_watches).
404not_foundNo game, set, card, webhook endpoint or watch with that id, or no endpoint at that path. The message says which.
409conflictThat webhook URL is already registered, or an identical price watch already exists.
429rate_limitedA limit was reached. details.limit says which: rate_limit (per minute) or monthly_allowance (a hard cap is set). Retry-After is in seconds.
429scan_hard_capCard recognition only: the monthly scan allowance is used up and a hard stop is set.
405—The path exists but not with that method. Empty body.
5xx—Our failure. Not billed. The body is not guaranteed to be JSON, so read the status before parsing.

Each endpoint’s page lists the errors it can return, with the request that causes each one and the response it got: for example GET /v1/cards.

More real responses

No key

curl "https://api.tcggraph.com/v1/games" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY"
401
{
  "error": {
    "code": "unauthenticated",
    "message": "Missing API key. Send `Authorization: Bearer <key>`.",
    "details": {
      "docs": "https://tcggraph.com/docs/authentication"
    }
  }
}

Unknown id

curl "https://api.tcggraph.com/v1/cards/pkm_nope_1" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY"
404
{
  "error": {
    "code": "not_found",
    "message": "No card with id \"pkm_nope_1\"."
  }
}

A path with no endpoint

curl "https://api.tcggraph.com/v1/card/pkm_sv3pt5_6" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY"
404
{
  "error": {
    "code": "not_found",
    "message": "No endpoint at GET /v1/card/pkm_sv3pt5_6.",
    "details": {
      "reference": "https://tcggraph.com/docs/rest",
      "openapi": "https://tcggraph.com/openapi.json"
    }
  }
}

Same watch twice

curl -X POST "https://api.tcggraph.com/v1/watches" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cardId":"pkm_sv3pt5_6","source":"cardmarket","direction":"below","threshold":20}'
409
{
  "error": {
    "code": "conflict",
    "message": "That watch already exists on this account."
  }
}

Handling errors

const response = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (!response.ok) {
  // A 405 has no body; everything else is JSON.
  const { error } = response.status === 405 ? { error: { code: "method_not_allowed" } } : await response.json();
  switch (error.code) {
    case "invalid_request":
      // error.details.field is the parameter; error.details.allowed the valid values.
      throw new Error(`${error.message} (${error.details?.field})`);
    case "rate_limited":
      if (error.details.limit === "rate_limit") {
        // Retry-After seconds, then try again.
      } else {
        // monthly_allowance (a hard cap is set): stop until the next period or the cap is lifted.
      }
      break;
    default:
      throw new Error(`${response.status} ${error.code}: ${error.message}`);
  }
}

What to retry

ResponseRetry?
429, details.limit = rate_limitYes, after Retry-After seconds (60).
429, monthly_allowanceNot in a loop: a hard cap lifts at the next period, or when you raise the plan or turn the cap off. See limits.
5xxYes, with exponential backoff.
402No. It needs a person: a plan (no_active_plan) or a top-up (balance_empty).
400, 401, 403, 404, 405, 409No. The same request gets the same answer; change the request.
async function withRetry(call, attempts = 4) {
  for (let i = 0; ; i++) {
    const response = await call();
    const retryable =
      response.status >= 500 ||
      (response.status === 429 && (await response.clone().json()).error.details?.limit === "rate_limit");
    if (!retryable || i === attempts - 1) return response;
    const after = Number(response.headers.get("Retry-After"));
    await new Promise((r) => setTimeout(r, Number.isFinite(after) && after > 0 ? after * 1000 : 2 ** i * 500));
  }
}

GraphQL errors

A GraphQL request refused before the document runs (no key, a limit) gets the envelope above. Once it runs, errors come back in GraphQL’s own errors array, with extensions.code set to BAD_REQUEST, GRAPHQL_VALIDATION_FAILED or invalid_request. The GraphQL reference has the full list and real responses.