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
| Status | Meaning | When it happens |
|---|---|---|
200 | OK | Request succeeded |
400 | Bad Request | Invalid parameters or malformed request body |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | API key valid but plan doesn’t include this resource |
404 | Not Found | Card, set, or resource doesn’t exist |
429 | Too Many Requests | Daily rate limit or burst limit exceeded |
500 | Internal Server Error | Something 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, ¬FoundErr):
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-Keyheader 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-Remainingon recent responses — you were close to the limit - Use batch lookups and cache responses to reduce request volume
- Wait for
Retry-Afterseconds before retrying
Getting 500?
- Transient server error — safe to retry with backoff
- If it persists, check status.tcgpricelookup.com
Best practices
- Always handle 429 explicitly — respect
Retry-After, implement exponential backoff with jitter - Branch on
error.code— it’s stable across releases;error.messageis not - Don’t retry 4xx errors (except 429) — they indicate a problem with your request
- Log both
codeandstatus— makes debugging much faster - Use SDK error types — they handle parsing, retries, and header inspection for you