Help us shape the future of Go! Take the 2026 Go Developer Survey

tcgplayer

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 16 Imported by: 2

README

go-tcgplayer

A small, practical Go client for the TCGplayer API (catalog & pricing). It handles OAuth2 client-credentials, automatic token refresh, rate limiting, and resilient HTTP via retryablehttp.

This repo also holds tcgdumper, the program that writes the nightly catalog dump every mtgban datastore is built from.


Features

  • Easy auth - fetches access_token using client-credentials and refreshes it before expiry.
  • Rate limiting - token-bucket guard around every request (defaults suitable for TCGplayer).
  • Resilient HTTP - built on hashicorp/go-retryablehttp.
  • Thread-safe - safe to use from multiple goroutines.
  • Tiny surface area - helpers for common catalog & pricing flows, plus a low-level Get for anything not wrapped yet.

Install

go get github.com/mtgban/go-tcgplayer

Quick start

package main

import (
    "context"
    "fmt"

    "github.com/mtgban/go-tcgplayer"
)

func main() {
    c, err := tcgplayer.NewClient("<PUBLIC_KEY>", "<PRIVATE_KEY>")
    if err != nil {
        // <handle err>
    }

    rows, err := c.GetMarketPricesByProducts(context.TODO(), []int{12345, 67890})
    if err != nil {
        // <handle err>
    }
    fmt.Println("retrieved rows:", len(rows))
}

Authentication

NewClient(publicKey, privateKey) wires a custom http.RoundTripper that:

  1. waits on a rate limiter,
  2. ensures a valid bearer token (refreshing if missing/near expiry),
  3. sets Authorization: Bearer <token> on each request.

Tokens are fetched from TCGplayer's /token endpoint using grant_type=client_credentials. Token acquisition is synchronized so bursts don't stampede the token endpoint.


Catalog helpers

  • Categories

    • GetCategoriesDetails(ctx context.Context, ids []int) ([]Category, error)
    • TotalCategories(ctx context.Context) (int, error)
  • Groups

    • ListAllCategoryGroups(ctx context.Context, category, offset int) ([]Group, error)
    • TotalGroups(ctx context.Context, category int) (int, error)
    • Groups(ctx context.Context, category int) iter.Seq2[Group, error]
  • Products

    • GetProductsDetails(ctx context.Context, ids []int, includeSkus bool) ([]Product, error)
    • ListAllProducts(ctx context.Context, category int, productTypes []string, includeSkus bool, offset int) ([]Product, error)
    • ListProductSKUs(ctx context.Context, productID int) ([]SKU, error)
    • TotalProducts(ctx context.Context, category int, productTypes []string) (int, error)
    • Products(ctx context.Context, category int, productTypes []string, includeSkus bool) iter.Seq2[Product, error]
  • Category metadata (decodes the ids referenced by SKUs)

    • ListCategoryPrintings(ctx context.Context, category int) ([]Printing, error)
    • ListCategoryConditions(ctx context.Context, category int) ([]Condition, error)
    • ListCategoryLanguages(ctx context.Context, category int) ([]Language, error)
    • ListCategoryRarities(ctx context.Context, category int) ([]Rarity, error)
Product types

ListAllProducts and TotalProducts filter by product type name, and the names belong to each game rather than to the platform. Yu-Gi-Oh files products under Tin and YGO Start Decks, which Magic never uses. Dragon Ball Super, UniVersus, Final Fantasy and Star Wars Destiny call their singles <Game> Singles, so asking them for Cards finds nothing. The API publishes no list of names and does not reject a wrong one: it answers with fewer products.

Ask the package for a category's names instead of writing them out:

  • ProductTypes(category) - every type the category files products under
  • SinglesProductTypes(category) - the type holding its single cards
  • SealedProductTypes(category) - all the others

A nil list means no filter at all, which returns every product in the category. An empty list is an error: SinglesProductTypes is empty for a category that sells no singles, such as supplies and storage, and SealedProductTypes is empty for one that sells only singles, such as Epic, so passing either on fails rather than quietly asking for everything.

To know a walk found everything, compare it with TotalProducts(ctx, category, nil). With no filter it counts products whose type no list names, which a count of the same names cannot see.

AllProductTypes() is every name in use across the platform, which makes it the wrong filter for any one category. Every one of these functions returns a fresh list, so editing it affects no other caller.


Pricing helpers

  • GetMarketPricesByProducts(ctx, productIds []int) ([]ProductPriceSet, error)
  • GetMarketPricesBySKUs(ctx, skuIds []int) ([]SKUPriceSet, error)

Each returns rows with the latest market pricing for the given IDs.


Pagination & limits

  • Offset + limit paging. Products and Groups walk a whole listing: they count first, page in steps of MaxItemsInResponse (100), and end with an error when a page holds other than its share of the count or repeats an id, since a page can answer short without an error of its own. ListAllProducts and ListAllCategoryGroups fetch one page, for a caller that pages for itself.

    for product, err := range c.Products(ctx, tcgplayer.CategoryLorcana, tcgplayer.SinglesProductTypes(tcgplayer.CategoryLorcana), true) {
        if err != nil {
            return err // what came before is not the whole category
        }
        // ...
    }
    
  • Batched IDs. Endpoints accept up to MaxIDsInRequest (250) IDs at a time. The client rejects more than that, and an empty list, before sending anything.


Error handling

Low-level Get() returns a BaseResponse envelope. High-level helpers decode BaseResponse.Results into typed slices.

  • On malformed JSON, you get a Go error carrying the raw body.
  • On non-2xx responses, the call returns an error: an *APIError holding the status and the envelope's errors when present, otherwise one built from the HTTP status and raw body.
  • A count of an empty result set is 0, not an error. The API answers one with a 404, and the Total* calls read that as zero.

tcgdumper

tcgdumper writes one category's whole catalog as a single JSON document, a CatalogDump: the category, its conditions, languages, printings and rarities, its groups, and every product with its skus. catalog-dump.yml runs it nightly and uploads each dump to B2, where datastore-gen, go-mtgban and mtgban-website read it.

It exits non-zero rather than pass off a short dump. Before fetching it counts the category with no product type filter, and it fails when the category's product types do not account for that count, when a page fails or comes back the wrong size, when the products and groups it collected do not match the counts it opened with, or when a product or group repeats or a product names a group the dump does not hold. All of it is checked before anything is written, so a failed run leaves no output. The workflow uploads only on success, so a failed night leaves the previous dump in place.

# Build
go build ./cmd/tcgdumper

# Run
TCGPLAYER_PUBLIC_KEY=... TCGPLAYER_PRIVATE_KEY=... ./tcgdumper -category 3 -thread 8 > pokemon.json

Flags:

  • -category (int, required) - Category ID to dump
  • -thread (int, default 8) - worker concurrency for paging products
  • -pub / -pri (string) - TCGplayer public/private keys; fall back to the TCGPLAYER_PUBLIC_KEY / TCGPLAYER_PRIVATE_KEY environment variables
  • -p / -pretty - indent the JSON output (default is a single line)
Reading a dump

Decode it into tcgplayer.CatalogDump. Every product carries productType, the type it was fetched by, which SinglesProductTypes and SealedProductTypes classify. Product.PresaleInfo.IsPresale marks a product not yet released, whose name and rarity may still change. Product.Extended(name) reads an extended data entry such as Number or Rarity, Group.ReleaseDate() gives the publish date without the time of day, and CatalogDump.PrintingNames() maps each product to the printings its skus are sold in. SPECIFICATIONS.md sets out the format and what a reader may rely on.


License

MIT

Documentation

Overview

Package tcgplayer is a client for the TCGplayer catalog and pricing API, and defines the format of the catalog dumps cmd/tcgdumper writes.

Index

Constants

View Source
const (
	// MaxItemsInResponse is the largest page a listing endpoint returns
	MaxItemsInResponse = 100
	// MaxIDsInRequest is the most ids a batched endpoint accepts
	MaxIDsInRequest = 250
)
View Source
const (
	CategoryMagic = iota + 1
	CategoryYuGiOh
	CategoryPokemon
	CategoryAxisAllies
	CategoryBoardgames
	CategoryDDMiniatures
	CategoryEpic
	CategoryHeroclix
	CategoryMonsterpocalypse
	CategoryRedakai
	CategoryStarWarsMiniatures
	CategoryWorldOfWarcraftMiniatures
	CategoryWoW
	CategorySupplies
	CategoryOrganizersStores
	CategoryCardfightVanguard
	CategoryForceOfWill
	CategoryDiceMasters
	CategoryFutureCardBuddyFight
	CategoryWeissSchwarz

	CategoryTCGplayer
	CategoryDragonBallZ
	CategoryFinalFantasy
	CategoryUniVersus
	CategoryStarWarsDestiny
	CategoryDragonBallSuper
	CategoryDragoborne
	CategoryFunko
	CategoryMetaX
	CategoryCardSleeves
	CategoryDeckBoxes
	CategoryCardStorageTins
	CategoryLifeCounters
	CategoryPlaymats
	CategoryZombieWorldOrder
	CategoryTheCasterChronicles
	CategoryMyLittlePony
	CategoryWarhammerBooks
	CategoryWarhammerBigBoxGames
	CategoryWarhammerBoxSets
	CategoryWarhammerClampacks
	CategoryCitadelPaints
	CategoryCitadelTools
	CategoryWarhammerGameAccessories
	CategoryBooks
	CategoryExodus
	CategoryLightseekers
	CategoryProtectivePages
	CategoryStorageAlbums
	CategoryCollectibleStorage
	CategorySupplyBundles
	CategoryMunchkin
	CategoryWarhammerAgeOfSigmarChampions
	CategoryArchitect
	CategoryBulkLots
	CategoryTransformers
	CategoryBakugan
	CategoryKeyForge
	CategoryChronoClashSystem
	CategoryArgentSaga
	CategoryFleshAndBlood
	CategoryDigimon
	CategoryAlternateSouls
	CategoryGateRuler
	CategoryMetaZoo
	CategoryWIXOSS
	CategoryOnePiece
	CategoryMarvelComics
	CategoryDCComics
	CategoryLorcana
	CategoryBattleSpiritsSaga
	CategoryShadowverseEvolve
	CategoryGrandArchive
	CategoryAkora
	CategoryKryptik
	CategorySorceryContestedRealm
	CategoryAlphaClash
	CategoryStarWarsUnlimited
	CategoryDragonBallSuperFusionWorld
	CategoryUnionArena
	CategoryTCGplayerSupplies
	CategoryElestrals
	CategoryNeopetsBattledome
	CategoryPokemonJapan
	CategoryGundam
	CategoryHololive
	CategoryGodzilla
	CategoryRiftbound
	CategoryCookieRunBraverse
	CategoryPalworld
	CategoryCyberpunk
)

All active categories on the platform

Variables

View Source
var (
	TokenURL = "https://api.tcgplayer.com/token"

	CatalogCategoriesURL = "https://api.tcgplayer.com/" + tcgAPIVersion + "/catalog/categories"
	CatalogProductsURL   = "https://api.tcgplayer.com/" + tcgAPIVersion + "/catalog/products"
	CatalogGroupsURL     = "https://api.tcgplayer.com/" + tcgAPIVersion + "/catalog/groups"

	PricingProductURL = "https://api.tcgplayer.com/" + tcgAPIVersion + "/pricing/product"
	PricingSKUURL     = "https://api.tcgplayer.com/" + tcgAPIVersion + "/pricing/sku"
)

Endpoint URLs, overridable for testing

Functions

func AllProductTypes

func AllProductTypes() []string

AllProductTypes returns every product type name the platform uses. No category files products under all of them, so it is not a filter for any one category; ProductTypes is.

func ProductTypes added in v0.1.2

func ProductTypes(category int) []string

ProductTypes returns the product types the given category files its products under, or every known type when the category is not listed.

func SealedProductTypes added in v0.1.2

func SealedProductTypes(category int) []string

SealedProductTypes returns the product types the given category files everything other than single cards under, sealed products above all. It is empty, never nil, for a category that sells only singles, such as Epic.

func SinglesProductTypes added in v0.1.2

func SinglesProductTypes(category int) []string

SinglesProductTypes returns the product types the given category files single cards under. It is empty for the categories that sell none, such as supplies and storage, and never nil: nil asks ListAllProducts and TotalProducts for every product, which is not what "no singles" means.

Types

type APIError added in v0.1.0

type APIError struct {
	StatusCode int
	Messages   []string
}

APIError is a request the API answered with an error envelope, keeping the http status the message arrived under

func (*APIError) Error added in v0.1.0

func (e *APIError) Error() string

Error joins the messages the API reported

type BaseResponse

type BaseResponse struct {
	TotalItems int             `json:"totalItems"`
	Success    bool            `json:"success"`
	Errors     []string        `json:"errors"`
	Results    json.RawMessage `json:"results"`
}

BaseResponse is the envelope every endpoint wraps its payload in. Results is left raw for the caller to decode into the type the endpoint returns.

type CatalogDump added in v0.1.0

type CatalogDump struct {
	Category   Category    `json:"category"`
	Conditions []Condition `json:"conditions"`
	Languages  []Language  `json:"languages"`
	Printings  []Printing  `json:"printings"`
	Rarities   []Rarity    `json:"rarities"`
	Groups     []Group     `json:"groups"`
	Products   []Product   `json:"products"`
}

CatalogDump is the envelope cmd/tcgdumper writes for a category. Naming it here keeps the program that writes a dump and the programs that read one on a single definition of the format, rather than each carrying its own.

func (*CatalogDump) PrintingNames added in v0.1.0

func (d *CatalogDump) PrintingNames() map[int][]string

PrintingNames maps each product to the distinct printing names its skus carry, ordered as the dump lists the category's printings. A printing the dump does not list for a product is one that product is not sold in.

type Category

type Category struct {
	CategoryID        int    `json:"categoryId"`
	Name              string `json:"name"`
	ModifiedOn        string `json:"modifiedOn"`
	DisplayName       string `json:"displayName"`
	SeoCategoryName   string `json:"seoCategoryName"`
	SealedLabel       string `json:"sealedLabel"`
	NonSealedLabel    string `json:"nonSealedLabel"`
	ConditionGuideURL string `json:"conditionGuideUrl"`
	IsScannable       bool   `json:"isScannable"`
	Popularity        int    `json:"popularity"`
	IsDirect          bool   `json:"isDirect"`

	// The storefront's own copy for the category's page
	CategoryDescription string `json:"categoryDescription"`
	CategoryPageTitle   string `json:"categoryPageTitle"`
}

Category is a game or product line the catalog is split into, such as Magic

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the TCGplayer API, holding the credentials every call needs. It acquires and refreshes bearer tokens on demand, holds requests to a rate limit, and retries the ones that fail transiently. A Client is safe for concurrent use.

func NewClient

func NewClient(publicKey, privateKey string) (*Client, error)

NewClient returns a Client authenticating with the given key pair. Both keys are required. No request is made here: the first token is acquired by the first call that needs one.

func (*Client) Get

func (tcg *Client) Get(ctx context.Context, link string) (*BaseResponse, error)

Get performs an authenticated GET request against link and parses the response envelope, leaving its payload for the caller to decode

func (*Client) GetCategoriesDetails

func (tcg *Client) GetCategoriesDetails(ctx context.Context, categoryIDs []int) ([]Category, error)

GetCategoriesDetails returns the details of the given categories, at most MaxIDsInRequest of them per call

func (*Client) GetMarketPricesByProducts

func (tcg *Client) GetMarketPricesByProducts(ctx context.Context, productIDs []int) ([]ProductPriceSet, error)

GetMarketPricesByProducts returns the current pricing of the given products, at most MaxIDsInRequest of them per call

func (*Client) GetMarketPricesBySKUs

func (tcg *Client) GetMarketPricesBySKUs(ctx context.Context, skuIDs []int) ([]SKUPriceSet, error)

GetMarketPricesBySKUs returns the current pricing of the given skus, at most MaxIDsInRequest of them per call

func (*Client) GetProductsDetails

func (tcg *Client) GetProductsDetails(ctx context.Context, productIDs []int, includeSkus bool) ([]Product, error)

GetProductsDetails returns the details of the given products, at most MaxIDsInRequest of them per call. Pass includeSkus to have each product carry the skus it is sold as.

func (*Client) Groups added in v0.2.0

func (tcg *Client) Groups(ctx context.Context, category int) iter.Seq2[Group, error]

Groups walks every group of a category as ListAllCategoryGroups pages them, with the checks Products makes.

func (*Client) ListAllCategoryGroups

func (tcg *Client) ListAllCategoryGroups(ctx context.Context, category, offset int) ([]Group, error)

ListAllCategoryGroups returns one page of a category's groups, starting at offset and holding at most MaxItemsInResponse of them. Pair it with TotalGroups to walk a whole category.

func (*Client) ListAllProducts

func (tcg *Client) ListAllProducts(ctx context.Context, category int, productTypes []string, includeSkus bool, offset int) ([]Product, error)

ListAllProducts returns one page of a category's products, starting at offset and holding at most MaxItemsInResponse of them, narrowed to the given product types. A nil list asks for every product; an empty one is an error. Pair it with TotalProducts to walk a whole category.

func (*Client) ListCategoryConditions added in v0.0.3

func (tcg *Client) ListCategoryConditions(ctx context.Context, category int) ([]Condition, error)

ListCategoryConditions returns the conditions a category's skus reference

func (*Client) ListCategoryLanguages added in v0.0.3

func (tcg *Client) ListCategoryLanguages(ctx context.Context, category int) ([]Language, error)

ListCategoryLanguages returns the languages a category's skus reference

func (*Client) ListCategoryPrintings

func (tcg *Client) ListCategoryPrintings(ctx context.Context, category int) ([]Printing, error)

ListCategoryPrintings returns the printings a category's skus reference

func (*Client) ListCategoryRarities added in v0.0.3

func (tcg *Client) ListCategoryRarities(ctx context.Context, category int) ([]Rarity, error)

ListCategoryRarities returns the rarities a category's products carry

func (*Client) ListProductSKUs

func (tcg *Client) ListProductSKUs(ctx context.Context, productID int) ([]SKU, error)

ListProductSKUs returns the skus a product is sold as

func (*Client) Products added in v0.2.0

func (tcg *Client) Products(ctx context.Context, category int, productTypes []string, includeSkus bool) iter.Seq2[Product, error]

Products walks every product of a category narrowed to productTypes, nil for every product, as ListAllProducts pages them. It counts first and checks each page against its share of that count, and each product id against those already yielded, so a page answering short, or a shift that repeats one product in place of another, ends the walk with an error rather than passing for a whole category. An error ends the walk: what was yielded before it is not the whole set.

func (*Client) TotalCategories

func (tcg *Client) TotalCategories(ctx context.Context) (int, error)

TotalCategories reports how many categories the platform holds

func (*Client) TotalGroups

func (tcg *Client) TotalGroups(ctx context.Context, category int) (int, error)

TotalGroups reports how many groups a category holds

func (*Client) TotalProducts

func (tcg *Client) TotalProducts(ctx context.Context, category int, productTypes []string) (int, error)

TotalProducts reports how many products a category holds, narrowed to the given product types. A nil list counts every product; an empty one is an error, since it names nothing to count.

type Condition added in v0.0.3

type Condition struct {
	ConditionID  int    `json:"conditionId"`
	Name         string `json:"name"`
	Abbreviation string `json:"abbreviation"`
	DisplayOrder int    `json:"displayOrder"`
}

Condition is a grade a category's products are sold in, such as Near Mint

type ExtendedField added in v0.2.0

type ExtendedField struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	Value       string `json:"value"`
}

ExtendedField is one extendedData entry, a key the catalog files under a product, such as Number or Rarity. Name is the key Product.Extended looks up; DisplayName is the storefront's label for it.

type Group

type Group struct {
	GroupID      int    `json:"groupId"`
	Name         string `json:"name"`
	Abbreviation string `json:"abbreviation"`
	Supplemental bool   `json:"isSupplemental"`
	PublishedOn  string `json:"publishedOn"`
	ModifiedOn   string `json:"modifiedOn"`
	CategoryID   int    `json:"categoryId"`
}

Group is a set, expansion or other collection a category files its products under

func (Group) ReleaseDate added in v0.1.0

func (g Group) ReleaseDate() string

ReleaseDate is the day the group was published, without the time of day

type Language added in v0.0.3

type Language struct {
	LanguageID   int    `json:"languageId"`
	Name         string `json:"name"`
	Abbreviation string `json:"abbr"`
}

Language is a language a category's products are printed in

type PresaleInfo added in v0.2.0

type PresaleInfo struct {
	IsPresale  bool   `json:"isPresale"`
	ReleasedOn string `json:"releasedOn,omitempty"`
	Note       string `json:"note,omitempty"`
}

PresaleInfo is a product's release status. ReleasedOn is set for presale products and for some already released, and Note carries the storefront's caveat on a presale, that details may change until release.

type Printing

type Printing struct {
	PrintingID   int    `json:"printingId"`
	Name         string `json:"name"`
	DisplayOrder int    `json:"displayOrder"`
	ModifiedOn   string `json:"modifiedOn"`
}

Printing is a finish a category's cards are printed in, such as Foil

type Product

type Product struct {
	ProductID  int    `json:"productId"`
	Name       string `json:"name"`
	CleanName  string `json:"cleanName"`
	ImageURL   string `json:"imageUrl"`
	GroupID    int    `json:"groupId"`
	CategoryID int    `json:"categoryId"`
	URL        string `json:"url"`
	ModifiedOn string `json:"modifiedOn"`
	ImageCount int    `json:"imageCount"`

	// Sent for every product, presale or not. Nil only in a dump written
	// before the package decoded it.
	PresaleInfo *PresaleInfo `json:"presaleInfo,omitempty"`

	// Never returned by the API, which does not report the product type a
	// product is filed under; tcgdumper stamps the type it fetched the
	// product by, so it is present in catalog dumps only
	ProductType string `json:"productType,omitempty"`

	// Only available for catalog API calls
	Skus []SKU `json:"skus,omitempty"`
	// Only available for catalog API calls
	ExtendedData []ExtendedField `json:"extendedData,omitempty"`
}

Product is a single item in the catalog, a card or a sealed product

func (Product) Extended added in v0.1.0

func (p Product) Extended(name string) string

Extended reads the extendedData entry a product carries under name, or "" when it carries none. The catalog files a card's collector number, rarity and the rest there rather than as fields of their own.

type ProductPriceSet

type ProductPriceSet struct {
	ProductID      int     `json:"productId"`
	LowPrice       float64 `json:"lowPrice"`
	MarketPrice    float64 `json:"marketPrice"`
	MidPrice       float64 `json:"midPrice"`
	HighPrice      float64 `json:"highPrice"`
	DirectLowPrice float64 `json:"directLowPrice"`
	SubTypeName    string  `json:"subTypeName"`
}

ProductPriceSet is the current pricing of one product, for one sub type

type Rarity added in v0.0.3

type Rarity struct {
	RarityID    int    `json:"rarityId"`
	DisplayText string `json:"displayText"`
	DBValue     string `json:"dbValue"`
}

Rarity is a rarity a category's cards are printed at

type SKU

type SKU struct {
	SKUID       int `json:"skuId"`
	ProductID   int `json:"productId"`
	LanguageID  int `json:"languageId"`
	PrintingID  int `json:"printingId"`
	ConditionID int `json:"conditionId"`
}

SKU is a sellable variant of a product: one combination of language, printing and condition

type SKUPriceSet

type SKUPriceSet struct {
	SKUID              int     `json:"skuId"`
	LowPrice           float64 `json:"lowPrice"`
	LowestShipping     float64 `json:"lowestShipping"`
	LowestListingPrice float64 `json:"lowestListingPrice"`
	MarketPrice        float64 `json:"marketPrice"`
	DirectLowPrice     float64 `json:"directLowPrice"`
}

SKUPriceSet is the current pricing of one sku

Directories

Path Synopsis
cmd
tcgdumper command
Command tcgdumper writes a category's whole TCGplayer catalog as one json document: its groups, its products with their skus, and the metadata naming the ids those skus carry.
Command tcgdumper writes a category's whole TCGplayer catalog as one json document: its groups, its products with their skus, and the metadata naming the ids those skus carry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL