Error Handling

HTTP status codes, error response format, common errors, and best practices for handling TCG Price Lookup API errors.


Error response format

All errors follow a consistent structure with no ambiguity:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error description.",
    "status": 400
  }
}

The code field is stable and machine-readable — safe to use in switch statements and conditional logic. The message field is human-readable and may change between releases. Always branch on code, not message.

HTTP status codes

StatusMeaningWhen it happens
200OKRequest succeeded
400Bad RequestInvalid parameters or malformed request body
401UnauthorizedMissing or invalid API key
403ForbiddenAPI key valid but plan doesn’t include this resource
404Not FoundCard, set, or resource doesn’t exist
429Too Many RequestsDaily rate limit or burst limit exceeded
500Internal Server ErrorSomething went wrong on our end — retry is safe

Do not retry 4xx errors except 429. They indicate a problem with your request, not a transient server issue.

Common errors

Missing API key — 401

{
  "error": {
    "code": "MISSING_API_KEY",
    "message": "The X-API-Key header is required.",
    "status": 401
  }
}

Fix: Include your API key in the X-API-Key header on every request. See authentication.

Invalid API key — 401

{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The provided API key is invalid or expired.",
    "status": 401
  }
}

Fix: Check your API key in the dashboard. Regenerate if expired.

Plan restriction — 403

{
  "error": {
    "code": "PLAN_RESTRICTION",
    "message": "Price history requires a Trader plan or above.",
    "status": 403
  }
}

Fix: Upgrade your plan. Price history requires Trader ($14.99/month) or Business ($89.99/month).

Card not found — 404

{
  "error": {
    "code": "CARD_NOT_FOUND",
    "message": "No card found with id 'invalid-id'.",
    "status": 404
  }
}

Fix: Verify the card ID. IDs come from search results — don’t construct them manually. Use the search endpoint to find valid IDs.

Invalid game parameter — 400

{
  "error": {
    "code": "INVALID_GAME",
    "message": "Invalid game 'digimon'. Supported: pokemon, mtg, yugioh, lorcana, onepiece, swu, fab, pokemonjp.",
    "status": 400
  }
}

Fix: Use one of the supported game identifiers listed in the message.

Rate limit exceeded — 429

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded your rate limit. Try again in 60 seconds.",
    "status": 429
  }
}

The response includes a Retry-After header with the number of seconds to wait. See rate limiting for details.

SDK error handling

All official SDKs throw typed exceptions that wrap the API error response:

// JavaScript / TypeScript
import { TCGLookup, TCGError, RateLimitError, NotFoundError } from 'tcglookup';

const tcg = new TCGLookup({ apiKey: process.env.TCG_API_KEY });

try {
  const card = await tcg.getCard('invalid-id');
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${err.retryAfter}s`);
  } else if (err instanceof NotFoundError) {
    console.log('Card does not exist');
  } else if (err instanceof TCGError) {
    console.log(`API error ${err.code}: ${err.message}`);
  } else {
    throw err; // Re-throw unexpected errors
  }
}
# Python
from tcglookup import TCGLookup, TCGError, RateLimitError, NotFoundError
import os

tcg = TCGLookup(api_key=os.environ["TCG_API_KEY"])

try:
    card = tcg.get_card("invalid-id")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except NotFoundError:
    print("Card does not exist")
except TCGError as e:
    print(f"API error {e.code}: {e.message}")
// Go
import (
    "errors"
    tcg "github.com/TCG-Price-Lookup/tcglookup-go"
)

card, err := client.GetCard("invalid-id")
if err != nil {
    var rateLimitErr *tcg.RateLimitError
    var notFoundErr *tcg.NotFoundError
    var apiErr *tcg.APIError

    switch {
    case errors.As(err, &rateLimitErr):
        fmt.Printf("Rate limited. Retry after %ds\n", rateLimitErr.RetryAfter)
    case errors.As(err, &notFoundErr):
        fmt.Println("Card does not exist")
    case errors.As(err, &apiErr):
        fmt.Printf("API error %s: %s\n", apiErr.Code, apiErr.Message)
    default:
        return err
    }
}
// Rust
use tcglookup::{Client, Error};

match client.get_card("invalid-id").await {
    Ok(card) => println!("{}", card.name),
    Err(Error::RateLimit { retry_after }) => {
        println!("Rate limited. Retry after {}s", retry_after);
    }
    Err(Error::NotFound) => {
        println!("Card does not exist");
    }
    Err(Error::Api { code, message, .. }) => {
        println!("API error {}: {}", code, message);
    }
    Err(e) => return Err(e.into()),
}

Retry strategy

For transient errors (429, 500), implement exponential backoff with jitter:

async function fetchWithRetry(fn, maxRetries = 4) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isRetryable = err.status === 429 || err.status >= 500;
      const isLastAttempt = attempt === maxRetries - 1;

      if (!isRetryable || isLastAttempt) throw err;

      // Respect Retry-After for 429, otherwise exponential backoff
      const baseDelay = err.retryAfter
        ? err.retryAfter * 1000
        : Math.pow(2, attempt) * 1000;

      // Add jitter to avoid thundering herd
      const jitter = Math.random() * 500;
      await new Promise(r => setTimeout(r, baseDelay + jitter));
    }
  }
}

// Usage
const card = await fetchWithRetry(() => tcg.getCard('pokemon-sv4-charizard-ex-006'));

Troubleshooting checklist

Getting 401?

  • Is the X-API-Key header present on every request?
  • Did you paste the full key (no extra spaces, no truncation)?
  • Log into the dashboard and check whether the key is active

Getting 403?

  • You’re calling an endpoint your plan doesn’t include
  • Price history requires Trader plan or above
  • Check the error message — it tells you what plan you need

Getting 404?

  • The card ID came from somewhere other than our search results
  • Check the ID format: {game}-{setCode}-{slug}
  • Run a search to confirm the card exists in our database

Getting 429?

  • Check X-RateLimit-Remaining on recent responses — you were close to the limit
  • Use batch lookups and cache responses to reduce request volume
  • Wait for Retry-After seconds before retrying

Getting 500?

Best practices

  1. Always handle 429 explicitly — respect Retry-After, implement exponential backoff with jitter
  2. Branch on error.code — it’s stable across releases; error.message is not
  3. Don’t retry 4xx errors (except 429) — they indicate a problem with your request
  4. Log both code and status — makes debugging much faster
  5. Use SDK error types — they handle parsing, retries, and header inspection for you