Skip to content

Repository files navigation

JustTCG JS/TS SDK

NPM Version Build Status License: MIT

The official JavaScript/TypeScript SDK for the JustTCG API. Access real-time and historical pricing data for the world's most popular Trading Card Games.

Features

  • ✅ Modern & Type-Safe: Written entirely in TypeScript for a superior developer experience with static typing and autocomplete.
  • 🔐 Versioned API Access: Clean, explicit access to API versions via client.v1 and client.v2, ensuring your integrations are stable and future-proof.
  • 🌍 v2 (public beta): Localized pricing across regions, graded cards, cursor pagination with iterate(), and a toV1Cards adapter so you can migrate without rewriting your types. See Using v2.
  • 🧼 Clean Data Models: Raw API responses are automatically transformed into clean, intuitive JavaScript objects (e.g., prices in dollars, camelCase properties).
  • 🚨 Robust Error Handling: Predictable, typed errors allow you to gracefully handle API issues like invalid keys or rate limits.
  • 🚀 Zero Dependencies: A lightweight package with no production dependencies for fast and secure installation.
  • 🤖 Node.js First: Optimized for server-side environments, with support for environment variable authentication out of the box.

Installation

npm install justtcg-js

Quick Start

Get your API key from your JustTCG Dashboard. For security, we recommend storing your API key in an environment variable named JUSTTCG_API_KEY.

import { JustTCG } from 'justtcg-js';

async function findTopCards() {
  try {
    // The client automatically looks for the JUSTTCG_API_KEY environment variable
    const client = new JustTCG();

    // Or, you can provide options directly in the constructor
    // const client = new JustTCG({ 
    //   apiKey: 'your_api_key_here',
    //   debug: true // Enable request logging
    // });

    console.log(`Searching for the most valuable cards in Disney Lorcana: The First Chapter...`);

    const response = await client.v1.cards.get({
      game: 'Disney Lorcana',
      set: 'the-first-chapter-disney-lorcana',
      orderBy: 'price',
      order: 'desc',
      limit: 10,
    });

    console.log('\n--- Top 10 Most Valuable Cards ---');
    response.data.forEach((card, index) => {
      // Find the most expensive variant for display (e.g., the Foil version)
      const topVariant = card.variants.sort((a, b) => (b.price ?? 0) - (a.price ?? 0))[0];

      const price = topVariant?.price?.toFixed(2) ?? 'N/A';
      const printing = topVariant?.printing ? `(${topVariant.printing})` : '';

      console.log(`${index + 1}. ${card.name} ${printing} - $${price}`);
    });

    console.log(`\nAPI requests remaining today: ${response.usage.apiDailyRequestsRemaining}`);
  } catch (error) {
    console.error('An error occurred:', (error as Error).message);
    process.exit(1);
  }
}

findTopCards();

The Response Object

All successful method calls from the client return a consistent JustTCGApiResponse object. It's crucial to understand its structure.

interface JustTCGApiResponse<T> {
  /** The main data payload from the API. The type of T depends on the method called. */
  data: T;

  /** Pagination metadata, ONLY present on paginated endpoints like `cards.get()` and `sets.list()`. */
  pagination?: {
    total: number;
    limit: number;
    offset: number;
    hasMore: boolean;
  };

  /** API usage metadata, included in every successful response. */
  usage: {
    apiRequestLimit: number;
    apiDailyLimit: number;
    apiRateLimit: number;
    apiRequestsUsed: number;
    apiDailyRequestsUsed: number;
    apiRequestsRemaining: number;
    apiDailyRequestsRemaining: number;
    apiPlan: string;
  };

  /** If the API returns an error (e.g., validation), this field will contain the message. */
  error?: string;

  /** An error code corresponding to the error message. */
  code?: string;
}

API Reference

The client is organized by API version, resource, and method. The structure is always: client.[version].[resource].[method](params).

v1.games

.list()

Fetches a list of all supported Trading Card Games.

  • Parameters: None.

  • Returns: Promise<JustTCGApiResponse<Game[]>>

Response data Object (Game):

{
  id: string; // The unique identifier for the game (e.g., 'pokemon')
  name: string; // The full name of the game (e.g., 'Pokemon')
  cards_count: number; // Total number of cards in the game
  sets_count: number; // Total number of sets in the game
  sealed_count: number; // Number of sealed products in the game
  last_updated: number; // Last updated timestamp (Unix seconds)
  game_value_usd: number; // Total value of the game's cards (USD)
  game_value_change_7d_pct: number; // Percentage change in game value over 7 days
  game_value_change_30d_pct: number; // Percentage change in game value over 30 days
  game_value_change_90d_pct: number; // Percentage change in game value over 90 days
  cards_pos_7d_count: number; // Number of cards with positive price change in 7d
  cards_neg_7d_count: number; // Number of cards with negative price change in 7d
  sealed_cards_pos_7d_count: number; // Number of sealed products with positive price change in 7d
  sealed_cards_neg_7d_count: number; // Number of sealed products with negative price change in 7d
  cards_pos_30d_count: number; // Number of cards with positive price change in 30d
  cards_neg_30d_count: number; // Number of cards with negative price change in 30d
  sealed_cards_pos_30d_count: number; // Number of sealed products with positive price change in 30d
  sealed_cards_neg_30d_count: number; // Number of sealed products with negative price change in 30d
  cards_pos_90d_count: number; // Number of cards with positive price change in 90d
  cards_neg_90d_count: number; // Number of cards with negative price change in 90d
  sealed_cards_pos_90d_count: number; // Number of sealed products with positive price change in 90d
  sealed_cards_neg_90d_count: number; // Number of sealed products with negative price change in 90d
}

v1.sets

.list(params)

Fetches a list of sets, which must be filtered by game.

  • Parameters:

    Parameter Type Required Description
    game string Yes The name of the game to filter sets by (e.g., 'Pokemon').
  • Returns: Promise<JustTCGApiResponse<Set[]>> (This response includes the pagination object).

Response data Object (Set):

{
  id: string; // The unique identifier for the set
  name: string; // The name of the set (e.g., 'Base Set')
  gameId: string; // The ID of the game this set belongs to
  game: string; // The name of the game this set belongs to
  count: number; // The number of cards in the set
  variants_count: number; // The total number of variants in the set
  sealed_count: number; // The number of sealed products in the set
  release_date: string; // The release date in ISO 8601 format
  set_value_usd: number; // Total value of the set's cards (USD)
  set_value_change_7d_pct: number; // Percentage change in set value over 7 days
  set_value_change_30d_pct: number; // Percentage change in set value over 30 days
  set_value_change_90d_pct: number; // Percentage change in set value over 90 days
}

v1.cards

.get(params)

A powerful and flexible method to browse, filter, and retrieve a paginated list of cards.

Parameter Type Description
tcgplayerId string A TCGplayer product ID to look up.
tcgplayerSkuId string The TCGplayer SKU ID for a specific variant.
cardId string A JustTCG card ID or UUID to look up.
variantId string A JustTCG variant ID or UUID to look up.
scryfallId string The Scryfall ID for the card (if applicable).
mtgjsonId string The MTGJSON ID for the card (if applicable).
query string A general search query for the card name.
game string The name of the game to filter by (e.g., 'Pokemon').
set string The id of the set to filter by (e.g., 'the-first-chapter-disney-lorcana').
number string The number of the card within the set (e.g., '015').
updated_after number Unix timestamp (seconds) to filter results updated after this time.
condition string[] An array of conditions to filter by. Supports fully spelled names or abbreviations. Full names: "Sealed", "Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged". Abbreviations: "S", "NM", "LP", "MP", "HP", "DMG".
printing string[] An array of print types to filter by (e.g., ['Foil', '1st Edition']).
include_price_history boolean Option to include price history in the response for matching variants.
priceHistoryDuration '7d' | '30d' | '90d' | '180d' | '1y' Specify which timeframe to include in the priceHistory array. This parameter is ignored if include_price_history is set to false.
include_statistics '7d' | '30d' | '90d' | '1y' | 'allTime' Specify which timeframe statistics to include in the response. Defaults to all timeframes. You can provide a comma-separated list (e.g., 7d,30d,1y) to include multiple statistics.
include_null_prices boolean Option to include cards that currently have null prices. Defaults to 'false'.
min_price number Filters results to include only cards where the current market price is greater than or equal to the provided value.
orderBy 'price' | '24h' | '7d' | '30d' | '90d' The field to sort the results by. Default is 'price'.
order 'asc' | 'desc' The sort order. Default is 'desc'.
limit number The maximum number of results to return. Default is 20.
PlanMax
Free20
Starter100
Professional100
Enterprise200
offset number The number of results to skip for pagination.
  • Returns: Promise<JustTCGApiResponse<Card[]>> (This response includes the pagination object).

.getByBatch(items)

Retrieves multiple specific cards and their variants in a single, efficient request. This is ideal for updating inventory prices.

  • Parameters: An array of BatchLookupItem objects.
Parameter Type Max Object Length Description
items BatchLookupItem[]
PlanMax
Free20
Starter100
Professional100
Enterprise200
An array of lookup objects.

BatchLookupItem Object:

You can mix and match different identifier types in a single batch request. The lookup item can define any combination of identifiers and options.

{
  tcgplayerId?: string; // A TCGplayer product ID.
  tcgplayerSkuId?: string; // The TCGplayer SKU ID for a specific variant.
  cardId?: string;      // A JustTCG card ID or UUID.
  variantId?: string;   // A JustTCG variant ID or UUID.
  scryfallId?: string;  // The Scryfall ID for the card.
  mtgjsonId?: string;   // The MTGJSON ID for the card.
  printing?: string[];  // Optional: Filter by specific print types for this item.
  condition?: string[]; // Optional: Filter by specific conditions for this item. Accepts full names and abbreviations.
  updated_after?: number; // Optional: Only return items updated after this Unix timestamp (seconds).
  include_price_history?: boolean; // Optional: Include price history for matched variants.
  include_statistics?: ('7d'|'30d'|'90d'|'1y'|'allTime')[]; // Optional: Specific timeframes for statistics.
}
  • Returns: Promise<JustTCGApiResponse<Card[]>> (This response does not include the pagination object).

Response data Object (Card and Variant)

The get() and getByBatch() methods return an array of Card objects. Each card contains an array of its Variant objects.

Card Object:

{
  /** The unique slug identifier for the card. Will be renamed to `slug` in v2. */
  id: string;
  /** The UUID of the card. Recommended for stable identification. */
  uuid: string;
  /** The name of the card. */
  name: string;
  /** The game this card belongs to. */
  game: string;
  /** The set ID this card belongs to. */
  set: string;
  /** The set name this card belongs to (may be omitted for some responses). */
  set_name?: string;
  /** The card number within the set. */
  number: string | null;
  /** The rarity of the card. */
  rarity: string | null;
  /** The TCGPlayer ID for the card. */
  tcgplayerId: string | null;
  /** The Scryfall ID for the card, if applicable. */
  scryfallId: string | null;
  /** The MTGJSON ID for the card, if applicable. */
  mtgjsonId: string | null;
  /** Additional details about the card. */
  details?: string | null;
  /** The different variants of the card. */
  variants: Variant[];
}

Variant Object: (Contains detailed pricing)

{
  /** The unique slug identifier for this variant. Will be renamed to `slug` in v2. */
  id: string;
  /** The UUID of this variant. Recommended for stable identification. */
  uuid: string;
  /** The condition of the card variant (e.g., Near Mint). */
  condition: string;
  /** The printing type of the card variant (e.g., Foil, 1st Edition). */
  printing: string;
  /** The language of the card variant, if applicable. */
  language: string | null;
  /** The TCGPlayer SKU of the specific variant, if available. */
  tcgplayerSkuId?: string;
  /** The current price of the card variant in dollars. */
  price: number;
  /** The last time the price was updated, as an epoch timestamp in seconds. */
  lastUpdated: number; // Epoch seconds
  /** The percentage change in price over the last 24 hours. */
  priceChange24hr?: number | null; // Percentage

  // --- 7d stats ---
  /** The percentage change in price over the last 7 days. */
  priceChange7d?: number | null; // Percentage
  /** The average price over the last 7 days. */
  avgPrice?: number | null; // Dollars
  /** The price history entries over the last 7 days. */
  priceHistory?: PriceHistoryEntry[] | null;
  minPrice7d?: number | null; // Dollars
  maxPrice7d?: number | null; // Dollars
  stddevPopPrice7d?: number | null;
  covPrice7d?: number | null;
  iqrPrice7d?: number | null;
  trendSlope7d?: number | null;
  priceChangesCount7d?: number | null;

  // --- 30d stats ---
  priceChange30d?: number | null; // Percentage
  avgPrice30d?: number | null; // Dollars
  minPrice30d?: number | null; // Dollars
  maxPrice30d?: number | null; // Dollars
  stddevPopPrice30d?: number | null;
  covPrice30d?: number | null;
  iqrPrice30d?: number | null;
  trendSlope30d?: number | null;
  priceChangesCount30d?: number | null;
  priceRelativeTo30dRange?: number | null;

  // --- 90d stats ---
  priceChange90d?: number | null; // Percentage
  avgPrice90d?: number | null; // Dollars
  minPrice90d?: number | null; // Dollars
  maxPrice90d?: number | null; // Dollars
  stddevPopPrice90d?: number | null;
  covPrice90d?: number | null;
  iqrPrice90d?: number | null;
  trendSlope90d?: number | null;
  priceChangesCount90d?: number | null;
  priceRelativeTo90dRange?: number | null;

  // --- 1y stats ---
  minPrice1y?: number | null;
  maxPrice1y?: number | null;

  // --- All-time stats ---
  minPriceAllTime?: number | null;
  minPriceAllTimeDate?: string | null;
  maxPriceAllTime?: number | null;
  maxPriceAllTimeDate?: string | null;
}

PriceHistoryEntry Object:

{
  /** Epoch timestamp in seconds. */
  t: number;
  /** Price in dollars. */
  p: number;
}

Using v2 (public beta)

v2 lives alongside v1 on the same client. Reaching for client.v2 never changes how client.v1 behaves — every v1 type, id, and response shape is untouched. Adopt v2 at your own pace, endpoint by endpoint.

import { JustTCG } from 'justtcg-js';

const client = new JustTCG();

// Search — returns the same { data, pagination, usage } envelope you know from v1
const { data } = await client.v2.cards.search('charizard', { game: 'pokemon', limit: 5 });

for (const card of data) {
  // markets[0] is the primary market — the drop-in replacement for v1's flat `price`
  console.log(card.name, card.variants[0]?.markets[0]?.price);
}

What's new in v2

Area v1 v2
Card id id is a slug; uuid is the UUID id is the UUID; slug holds the old v1 id
Pricing flat price, avgPrice30d, … on the variant a markets[] array, one entry per region; stats grouped under markets[].periods
Localization US/NA only request regions: ['UK', 'US'] — prices are the real local prices, never currency-converted
Graded cards not returned ordinary variants with type: 'graded' and a grading object
Pagination offset + total opaque cursor + Link header; walk it with iterate()
Usage _metadata in the body RateLimit-* response headers, surfaced as usage
Errors error/code on the response thrown typed exceptions (NotFoundError, RateLimitError, …)

Nested fields also regroup: game becomes { id, name }, set becomes { id, name }, and the third-party ids move under external_ids (external_ids.tcgplayer, external_ids.scryfall, …).

Methods

All methods return { data, pagination?, usage }. pagination is present on list methods (get, search) and absent on the others.

// Browse / filter — with no params, this browses; with q/number it searches
await client.v2.cards.get({ game: 'pokemon', regions: ['US'], limit: 20 });

// Search by name
await client.v2.cards.search('pikachu', { game: 'pokemon' });

// Direct lookup — accepts a v2 UUID or a legacy v1 slug, and unwraps the single card.
// This is the only lookup that accepts graded: 'include'.
await client.v2.cards.retrieve('907005b3-b7bd-5eac-aed9-d20454b7fe8a');
await client.v2.cards.retrieveVariant('e72a67fe-922e-5662-bcfc-e8cb509f8220');

// Batch — snake_case body keys (a v1 payload needs its keys renamed, not sent as-is).
// `regions` applies to every item and is sent once.
await client.v2.cards.getByBatch([{ card_id: '…' }, { tcgplayer_id: '42' }], { regions: ['US'] });

Iterating every page

iterate() walks the entire result set one card at a time, fetching pages lazily — breaking out of the loop stops the requests, so it's safe to point at a large set. Use iteratePages() when you need the page boundaries or want to watch usage as you go.

for await (const card of client.v2.cards.iterate({ game: 'pokemon', limit: 100 })) {
  console.log(card.name);
}

for await (const page of client.v2.cards.iteratePages({ game: 'pokemon' })) {
  console.log(page.data.length, 'cards,', page.usage.remaining, 'requests left');
}

Localized pricing

Request regions in priority order; markets come back in the same order, and markets[0] is the market that min_price and order_by apply to. A region with no local data has price: null — the SDK never falls back to a different currency.

const { data } = await client.v2.cards.search('charizard', {
  game: 'pokemon',
  regions: ['UK', 'US'],
});

for (const market of data[0].variants[0].markets) {
  console.log(market.region, market.price, market.currency); // 'UK' 410.5 'GBP'
}

Omitting regions yields a single US market (USD), reproducing v1's pricing. Extra regions and graded: 'include' carry a cost surcharge; exact figures are set at launch.

Migrating without rewriting your types

If your codebase is already typed against the v1 Card, switch the fetch to v2 and run the results through toV1Cards — it reshapes v2 back into the exact v1 shape, so nothing downstream changes.

import { JustTCG, toV1Cards } from 'justtcg-js';
import type { Card } from 'justtcg-js';

const { data } = await client.v2.cards.get({ game: 'pokemon' });
const legacy: Card[] = toV1Cards(data); // flat price, avgPrice, game as a string — all v1 names

// Choose which region flattens into the price fields (defaults to the primary market):
const ukPriced = toV1Cards(data, { region: 'UK' });

Two things deliberately don't round-trip: graded variants are dropped unless you pass { includeGraded: true } (v1 was raw-only), and printing loses the " - <Language>" suffix v1 appended for non-English cards (v2 carries language separately). Use toV1Card for a single card and toV1Variant for a single variant.

Note: External-id GET parameters (tcgplayerId, scryfallId, …) are pending on the v2 get endpoint. Until then, use them via getByBatch or stay on client.v1 for those lookups.

Error Handling

The SDK surfaces errors in two primary ways: thrown exceptions for critical SDK issues and an error property on the response object for API issues.

v2 note: v2 never uses the error property — it always throws a typed exception on failure. The classes are exported (JustTCGError and its subclasses AuthenticationError, ValidationError, RegionNotAvailableError, NotFoundError, RateLimitError, ApiError) so you can branch with instanceof. Each carries .status, .code, and the raw .problem body; RateLimitError adds .retryAfter, RegionNotAvailableError adds .available.

import { NotFoundError, RateLimitError } from 'justtcg-js';

try {
  await client.v2.cards.retrieve(cardId);
} catch (error) {
  if (error instanceof NotFoundError) { /* no such card */ }
  else if (error instanceof RateLimitError) { console.log(`retry after ${error.retryAfter}s`); }
  else throw error;
}

1. Thrown Exceptions (SDK-level Errors)

Critical errors that happen at the SDK level will be thrown as a JavaScript Error. These must be wrapped in a try...catch block.

Common Causes:

  • Authentication Failure (401): The API key is missing.
  • Invalid Parameter Value: Providing an unknown value for a parameter like orderBy.

Example:

try {
  const client = new JustTCG();
  await client.v1.games.list();
} catch (error) {
  // This block will catch the error
  console.error((error as Error).message); // e.g., "Authentication error: API key is missing."
}

2. Response error Property (API-level Errors)

If a request is syntactically valid but fails API-level validation (e.g., an invalid lookup parameter), the API will often return a normal response object where the data is empty and the error and code properties are populated.

Common Causes:

  • Authentication Failure (401/403): The provided API key is invalid or disabled.
  • Not Found (404): The requested endpoint does not exist.
  • Rate Limit Exceeded (429): Your application is making too many requests.
  • Missing Required Parameters: Calling client.v1.sets.list() without the required game parameter.
  • Invalid Lookup Parameter: Providing an unknown value for a parameter like cardId.

Example:

const client = new JustTCG();
// Missing the required 'game' parameter
const response = await client.v1.sets.list();

if (response.error) {
  // This block will execute
  console.log(response.error); // "Required query parameter \"game\" is missing"
  console.log(response.code);  // "INVALID_REQUEST"
} else {
  // This data will be empty
  console.log(response.data);
}

Examples

You can find practical, runnable examples in the /examples directory of this repository.

To run an example, first ensure you have set your JUSTTCG_API_KEY environment variable.

# Example: Find the most valuable Lorcana cards from The First Chapter
export JUSTTCG_API_KEY="YOUR_API_KEY_HERE"
npx ts-node examples/find-most-valuable-lorcana-cards.ts

For the v2 API (public beta):

npx ts-node examples/v2-quickstart.ts           # search, direct lookup, localized pricing
npx ts-node examples/v2-iterate-and-graded.ts   # cursor iteration and graded cards
npx ts-node examples/v2-migrate-from-v1.ts      # toV1Cards adapter and typed errors

License

This SDK is licensed under the MIT License.

About

The official JavaScript/TypeScript SDK for the JustTCG API.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages