# PokéWallet API Documentation

Source: https://pokewallet.io/api-docs

Complete REST reference for the most comprehensive Pokémon TCG platform. Real-time pricing, 50,000+ cards database, and generous rate limits.

In short

PokéWallet is a REST API for Pokémon TCG card prices and data: 50,000+ cards with TCGPlayer (USD) and CardMarket (EUR) prices, refreshed daily, plus sets, search and card images. The free tier (100 requests/hour) only needs an API key; price history and set analytics need the Pro plan (€20/month) or its 7-day trial.

At a glance

- **Base URL:** `https://api.pokewallet.io`
- **Cards:** 50,000+ Pokémon TCG cards, plus sealed products priced on TCGPlayer
- **Card IDs:** `pk_` + hex for cards on TCGPlayer; hex without prefix for CardMarket-only cards
- **Prices:** TCGPlayer (USD) for every variant and CardMarket (EUR) for `normal` and `holo`, refreshed daily
- **Free on every plan:** Sets, set cards, card details, search and images
- **Pro plan or 7-day trial:** Price history, prices by set, set statistics, completion value, trending sets and top cards
- **API key:** One key for all 12 games, with shared rate limits

Updated Oct 19, 2026

## Quick Start

Get started with PokéWallet API in 4 simple steps:

### Create an Account

Sign up for free to start using the API. Upgrade anytime from your dashboard.

[Sign Up Now](https://pokewallet.io/auth/login?source=pokemon)

### Generate an API Key

Create your first API key from the dashboard.

### Make Your First Request

Start querying the API with your key.

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
       "https://api.pokewallet.io/search?q=charizard"
```

### Join the Discord Community

Stay up to date with new features, ask for help, and connect with other developers.

[Join Discord](https://discord.gg/znDxEjmDVP)

## Authentication

All API requests (except `/health`) require authentication using an API key.

### API Key Format

API keys follow this format:

- **Production keys:** `pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
- **Development keys:** `pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`

> **Security Warning:** Never expose your API keys in client-side code, public repositories, or logs. Always use environment variables.

### Authentication Methods

You can authenticate using either the `X-API-Key` header or `Authorization` header:

*X-API-Key Header (Recommended)*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/search?q=pikachu"
```

*Authorization Header*

```bash
curl -H "Authorization: Bearer pk_live_your_key_here" \
     "https://api.pokewallet.io/search?q=pikachu"
```

## Rate Limits

Rate limits vary depending on your subscription plan:

| Plan | Per hour | Per day | Paid endpoints | Price |
| --- | --- | --- | --- | --- |
| Free | 100 | 1,000 | — | €0 |
| Coffee | 1,000+ | 10,000+ | — | Buy a coffee on Ko-fi |
| Pro | 5,000 | 50,000 | Pokémon + One Piece | €20/month |
| Ultra | 7,000 | 70,000 | All 12 games | €29/month |
| Business | Custom | Custom | All 12 games | Contact us |
| Early Access | 1,000 | 10,000 | — | No longer available |

> All new users start on the **Free plan**. Upgrade from your [dashboard](https://pokewallet.io/dashboard). Early Access was our initial launch tier and is no longer available for new users.

> **Please note:** After upgrading your plan, activation may require a couple of hours.

[See all plans, pricing and FAQs](https://pokewallet.io/pricing)

### Rate Limit Headers

Every API response includes rate limit information:

```
X-RateLimit-Limit-Hour: 100
X-RateLimit-Remaining-Hour: 95
X-RateLimit-Limit-Day: 1000
X-RateLimit-Remaining-Day: 823
```

### Rate Limit Exceeded (429)

When you exceed your rate limit, you'll receive a `429 Too Many Requests` response:

```json
{
  "error": "Rate limit exceeded",
  "message": "Hourly limit exceeded",
  "limits": {
    "hourly": {
      "limit": 100,
      "used": 100,
      "remaining": 0
    },
    "daily": {
      "limit": 1000,
      "used": 523,
      "remaining": 477
    }
  }
}
```

## Public Endpoints

> **No Authentication Required:** These endpoints are publicly accessible.

### GET /health (No Auth Required)

Check API status, database, cache, and storage health with response times.

### Example Request:

*cURL*

```bash
curl "https://api.pokewallet.io/health"
```

### Response Example (Healthy):

*200 OK*

```json
{
  "status": "healthy",
  "timestamp": "2025-12-16T10:30:00.000Z",
  "version": "1.1.0",
  "checks": {
    "database": {
      "status": "healthy",
      "responseTime": 45
    },
    "cache": {
      "status": "healthy",
      "responseTime": 3
    },
    "storage": {
      "status": "healthy",
      "responseTime": 12
    }
  },
  "responseTime": 60,
  "region": "FRA"
}
```

### Response Example (Unhealthy):

*503 Service Unavailable*

```json
{
  "status": "unhealthy",
  "timestamp": "2025-12-16T10:30:00.000Z",
  "version": "1.1.0",
  "message": "Critical services unavailable - API may not function correctly",
  "checks": {
    "database": {
      "status": "unhealthy",
      "responseTime": null,
      "message": "Connection failed"
    },
    "cache": {
      "status": "healthy",
      "responseTime": 3
    },
    "storage": {
      "status": "healthy",
      "responseTime": 12
    }
  },
  "responseTime": 1050,
  "region": "FRA"
}
```

### GET / (No Auth Required)

Get API information, version, and available endpoints.

### Example Request:

*cURL*

```bash
curl "https://api.pokewallet.io/"
```

### Response Example:

*200 OK*

```json
{
  "name": "PokeWallet API",
  "version": "1.1.0",
  "status": "production",
  "authentication": {
    "required": true,
    "method": "API Key",
    "header": "X-API-Key",
    "format": "pk_live_xxxxxxxxxxxxx"
  },
  "endpoints": {
    "health": { "path": "/health", "auth_required": false },
    "search": { "path": "/search?q=charizard&limit=20", "auth_required": true },
    "cardDetail": { "path": "/cards/:id", "auth_required": true },
    "sets": { "path": "/sets", "auth_required": true },
    "setDetails": { "path": "/sets/:setCode?page=1&limit=50", "auth_required": true },
    "images": { "path": "/images/:id?size=high", "auth_required": true }
  },
  "rate_limits": {
    "per_hour": 100,
    "per_day": 1000,
    "plan": "free"
  }
}
```

### GET /games (No Auth Required)

Lists the games available under their own prefix (`/mtg`, `/ygo`, …) with their routes. Pokémon and One Piece are not in this list: they use `/cards`, `/sets`… and `/op/…`.

### Example Request:

*cURL*

```bash
curl "https://api.pokewallet.io/games"
```

### Response Example (first of ten games):

*200 OK*

```json
{
  "success": true,
  "data": [
    {
      "code": "mtg",
      "name": "Magic: the Gathering",
      "endpoints": {
        "sets": "/mtg/sets",
        "set": "/mtg/sets/:set",
        "card": "/mtg/cards/:id",
        "search": "/mtg/search?q=",
        "image": "/mtg/images/:id"
      }
    }
  ]
}
```

> **Pokémon TCG PRO endpoints: Pro or Ultra plan**
>
> Pro (€20/month) covers Pokémon and One Piece only. Ultra (€29/month) includes everything in Pro plus the paid endpoints of the other 10 games: Magic, Yu-Gi-Oh!, Lorcana and more.
>
> [Compare plans →](https://pokewallet.io/pricing)

## Card ID Formats

The API uses two different ID formats depending on the card's data source:

### TCG Cards (TCGPlayer)

**Format:** `pk_` + hexadecimal hash

**Example:**

```
pk_72046138a4c1908a9f27c93fdd8189ba...
```

**Usage:** All cards with TCGPlayer data (even if they also have CardMarket data)

> **One ID per printing:** alternate printings of the same card number (e.g. Pikachu 025/165 base, Poké Ball Pattern, Master Ball Pattern) have different IDs. Finishes of the same product (Normal / Reverse Holofoil) share one ID.

### CardMarket-Only Cards

**Format:** Direct hexadecimal hash (no `pk_` prefix)

**Example:**

```
2208e8a2750c07d89e54870ffcef80d1...
```

**Usage:** Only cards exclusively from CardMarket (e.g., Japanese sets, European promos)

> **Unique Variants:** Cards with the same `card_number` but different variants (V1, V2, etc.) have unique IDs.

## Price Variants

Prices come from two different sources, each with its own way of classifying card variants. Understanding this distinction is important when reading the `prices` array in any response.

### TCGPlayer

Uses a `sub_type_name` field to distinguish versions:

- **`Normal`:** Common / non-holo cards
- **`Holofoil`:** Holo cards
- **`Reverse Holofoil`:** Most Vintage Sets (Base through Neo)
- **`1st Edition`:** Base Set 1st Ed and similar
- **`Unlimited`:** Standard version of sets that have 1st editions
- **`Shadowless`:** Specific Base Set variants

### CardMarket

Uses a simpler `variant_type` field:

- **`normal`:** Standard non-holo version
- **`holo`:** Holo version

> CardMarket uses a simpler two-tier system regardless of the set era.

## Product Types

TCGPlayer also lists sealed products — booster boxes, elite trainer boxes, blisters, tins, displays, theme decks — alongside singles. The API surfaces both, labeled by a `product_type` field.

### Cards

Singles — the default. Have a `rarity`, appear in price statistics, completion value and top-cards rankings.

### Sealed Products

Booster boxes, ETBs, blisters, tins, displays, theme decks. Priced only via TCGPlayer — `cardmarket` is always `null`, since CardMarket sealed data isn't imported yet.

> **Where `product_type` appears:** inside `card_info` on `/search`, `/sets/:setCode` and `/cards/:id`; at the row level (alongside `id`, `card_number`, `name`) on `/prices/:setCode`.

> **Excluded from aggregates:** sealed products are omitted from `/analytics/top-cards`, `/sets/:setCode/statistics` and `/sets/:setCode/completion-value` — a booster box's price would otherwise dominate rankings and completion-cost estimates meant for singles. They still appear in `/sets` (counted separately via `sealed_count`) and `/sets/:setCode` (listed alongside the cards).

## Search Endpoints

> **Authentication Required:** All Search endpoints require an API key.

### GET /search

Advanced card search with unified pricing from TCGPlayer and CardMarket.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | Required | Search query. Supports multiple formats: **Card name:** `charizard`, `pikachu ex`; **Set code:** `SV2a`, `SWSH3`; **Card number:** `148`, `148/165`; **set_id + card number:** `24541 148` — precise lookup using a numeric set ID and card number |
| `page` | number | Optional | Page number (default: 1) |
| `limit` | number | Optional | Results per page (default: 20, max: 100) |

### Example Requests:

*cURL — Search by name*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/search?q=charizard+ex"
```

*cURL — Search by set_id + card number (precise lookup)*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/search?q=24541%20148"
```

### Response Example (TCG Card):

*200 OK*

```json
{
  "query": "pikachu",
  "results": [
    {
      "id": "pk_72046138a4c1908a9f27c93fdd8189ba4ac8e683efaed6b9161efcef129302394a9ec1d20d",
      "card_info": {
        "name": "Pikachu ex (Pikachu 60)",
        "clean_name": "Pikachu ex Pikachu 60",
        "set_name": "Battle Academy 2024",
        "set_code": "BA2024",
        "set_id": "23520",
        "card_number": "106",
        "rarity": "Holo Rare",
        "card_type": "Lightning",
        "hp": "200.0",
        "stage": "Basic",
        "card_text": null,
        "attacks": ["[LLC] Thunderbolt (120)"],
        "weakness": "Fx2",
        "resistance": null,
        "retreat_cost": "1.0",
        "product_type": "card"
      },
      "images": {
        "languages": ["en", "it", "de"]
      },
      "tcgplayer": {
        "prices": [{
          "sub_type_name": "Normal",
          "low_price": 7,
          "mid_price": 9.37,
          "high_price": 18,
          "market_price": 8.01,
          "direct_low_price": null,
          "updated_at": "2025-12-27T05:14:43.451818"
        }],
        "url": "https://www.tcgplayer.com/product/556443"
      },
      "cardmarket": null
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 56,
    "total_pages": 3
  },
  "metadata": {
    "total_count": 56,
    "tcg": 18,
    "cardmarket": 38,
    "tcg_only": 18,
    "cardmarket_only": 38,
    "both_sources": 0
  }
}
```

### Response Example (CardMarket-Only Card):

*200 OK*

```json
{
  "query": "cubone cbb3c",
  "results": [
    {
      "id": "8eb728bd59d4e1a0d94c354e4cc65a7465b55cfa4261bfa93301faa5ec921f97",
      "card_info": {
        "name": "Cubone (CBB3C 04)",
        "set_code": "CBB3C",
        "set_id": "-15",
        "card_number": "4",
        "product_type": "card"
      },
      "images": {
        "languages": ["en"]
      },
      "tcgplayer": null,
      "cardmarket": {
        "product_name": "Cubone (CBB3C 04)",
        "prices": [
          {
            "avg": null,
            "low": 0.02,
            "avg1": null,
            "avg7": null,
            "avg30": null,
            "trend": 0,
            "updated_at": "2025-12-29T04:54:57.728497",
            "variant_type": "normal"
          },
          {
            "avg": 0.37,
            "low": 0.02,
            "avg1": 0.1,
            "avg7": 0.35,
            "avg30": 0.35,
            "trend": 0.29,
            "updated_at": "2025-12-29T04:54:57.728497",
            "variant_type": "holo"
          }
        ],
        "product_url": "https://www.cardmarket.com/en/Pokemon/Products/Singles/Gem-Pack-Vol-3/Cubone-V1-CBB3C04"
      }
    },
    {
      "id": "a7ed43b641d4f24b494ad5889845085f33966d1f19cb9e0ed353979025d8da31",
      "card_info": {
        "name": "Cubone (CBB3C 04)",
        "set_code": "CBB3C",
        "set_id": "-15",
        "card_number": "4",
        "product_type": "card"
      },
      "images": {
        "languages": ["en"]
      },
      "tcgplayer": null,
      "cardmarket": {
        "product_name": "Cubone (CBB3C 04)",
        "prices": [
          {
            "avg": null,
            "low": 0.02,
            "trend": 0,
            "variant_type": "normal"
          }
        ],
        "product_url": "https://www.cardmarket.com/en/Pokemon/Products/Singles/Gem-Pack-Vol-3/Cubone-V4-CBB3C04"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 2,
    "total": 7,
    "total_pages": 4
  },
  "metadata": {
    "total_count": 7,
    "tcg": 0,
    "cardmarket": 7,
    "tcg_only": 0,
    "cardmarket_only": 7,
    "both_sources": 0
  }
}
```

> **Note on CardMarket-Only Cards:** The 7 Cubone variants (V1-V7) all share the same `card_number: "4"` and `set_code: "CBB3C"`, but each has a unique ID. This ensures each variant can be uniquely identified and its correct image can be retrieved. **Set Identifiers:** Use `set_id` to match cards to their set: it is the same value returned by the `/sets` endpoint. `card_info.set_code` may contain the numeric set id instead of the short code for some cards. **Image Languages:** The `images.languages` array lists the languages the card's image is available in. `"en"` is always present and refers to the card's default artwork (English for international sets, Japanese for Japanese sets); additional codes (`it`, `fr`, `de`, `es`, `pt`) mean a localized European variant can be requested via `/images/:id?lang=xx`. See the [Images Endpoint](https://pokewallet.io/api-docs#images-endpoint). **Sealed Products:** Results can include sealed products (booster boxes, ETBs, displays, etc.) as well as cards — check `card_info.product_type`. CardMarket-only results are always `"card"`, since CardMarket sealed data isn't imported yet. See [Product Types](https://pokewallet.io/api-docs#product-types).

### GET /search/by-price-range (Coming Soon)

Search cards by price range with optional filters for rarity and set.

> **⏳ This endpoint is currently in development** and will be available soon after thorough testing.

### GET /search/by-type (Coming Soon)

Search cards by card type (Pokemon, Trainer, Energy) and optional subtype filters.

> **⏳ This endpoint is currently in development** and will be available soon after thorough testing.

## Cards Endpoints

> **Authentication Required:** All Cards endpoints require an API key in the `X-API-Key` header.

### GET /cards/:id

Get complete card details with unified pricing from TCGPlayer and CardMarket.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Required | Card ID - Two formats supported: **TCG cards:** `pk_xxx` (with prefix); **CardMarket-only:** hexadecimal hash (no prefix) |
| `set_code` | string | Optional | Set code for disambiguation (e.g., "SWSH3", "SV1", "CBB3C"). Supports both alphanumeric codes and numeric group IDs. |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/cards/pk_xxx"
```

### Response Example:

*200 OK*

```json
{
  "id": "pk_xxx",
  "card_info": {
    "name": "Charizard VMAX",
    "clean_name": "Charizard VMAX",
    "set_name": "Darkness Ablaze",
    "set_code": "SWSH3",
    "set_id": "22789",
    "card_number": "20/189",
    "rarity": "Secret Rare",
    "card_type": "Pokemon",
    "hp": "330",
    "stage": null,
    "card_text": "VMAX rule: When your VMAX Pokemon is Knocked Out...",
    "attacks": ["Fire Spin - 320 damage"],
    "weakness": "Water",
    "resistance": null,
    "retreat_cost": "3",
    "product_type": "card"
  },
  "images": {
    "languages": ["en", "it", "fr", "de", "es"]
  },
  "tcgplayer": {
    "url": "https://tcgplayer.com/product/123456",
    "prices": [
      {
        "sub_type_name": "Normal",
        "low_price": 245.99,
        "mid_price": 299.99,
        "high_price": 450.00,
        "market_price": 285.00,
        "direct_low_price": 250.00,
        "updated_at": "2025-12-16T04:00:00Z"
      }
    ]
  },
  "cardmarket": {
    "product_name": "Charizard VMAX (Secret)",
    "product_url": "https://cardmarket.com/product/789012",
    "prices": [{
      "variant_type": "normal",
      "avg": 260.50,
      "low": 240.00,
      "trend": 270.00,
      "avg1": 258.30,
      "avg7": 255.80,
      "avg30": 280.50,
      "updated_at": "2025-12-16T04:00:00Z"
    }]
  }
}
```

### GET /cards/:id/price-history (Pro)

Get historical price snapshots from both TCGPlayer and CardMarket, with percentage changes over 7, 14, 30, 60 and 120 days.

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Required | Card ID (`pk_xxx` for TCG+CM cards, `encrypted_id` for CM-only cards) |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/cards/pk_xxx/price-history"
```

### Response Example:

*200 OK*

```json
{
  "id": "pk_abc123",
  "card_name": "Charizard (Base Set)",
  "tcgplayer": {
    "variants": {
      "Normal": {
        "current": { "market": 12.50, "low": 10.00, "mid": 11.50, "high": 15.00, "direct_low": 11.00 },
        "price_7d": 11.80,
        "price_14d": 11.20,
        "price_30d": null,
        "price_60d": null,
        "price_120d": null,
        "change_7d": "+5.9%",
        "change_14d": "+11.6%",
        "change_30d": null,
        "change_60d": null,
        "change_120d": null,
        "last_updated": "2026-04-26T06:00:00Z"
      }
    }
  },
  "cardmarket": {
    "variants": {
      "normal": {
        "current": { "avg": 9.50, "low": 8.00, "trend": 9.20 },
        "avg_1d": 9.40,
        "avg_7d": 8.90,
        "avg_30d": 8.50,
        "price_7d": 8.90,
        "price_14d": 8.60,
        "price_30d": null,
        "price_60d": null,
        "price_120d": null,
        "change_7d": "+6.7%",
        "change_14d": "+10.5%",
        "change_30d": null,
        "change_60d": null,
        "change_120d": null,
        "last_updated": "2026-04-26T06:00:00Z"
      }
    }
  }
}
```

> Fields like `price_14d`, `price_30d`, `price_60d`, `price_120d` return `null` until enough weekly snapshots have accumulated. `price_7d` is available from April 16, 2026.

### Trial Response Headers:

When accessing this endpoint during an active trial, two extra headers are included:

```
X-Trial-Expires-At: 2026-05-01T00:00:00.000Z
X-Trial-Days-Remaining: 7
```

### Error Responses:

| Status | Description |
| --- | --- |
| `403` — trial not activated | `{ "error": "Trial not activated", "redeem_url": "https://pokewallet.io/dashboard" }` |
| `403` — trial expired | `{ "error": "Pro plan required", "upgrade_url": "https://pokewallet.io/pricing" }` |

## Sets Endpoints

> **Authentication Required:** All Sets endpoints require an API key.

### GET /sets

Get list of all Pokemon sets with card counts.

### Response Fields:

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Set name |
| `set_code` | string \| null | Short code for the set (e.g., "SV1", "SWSH3"). May be `null` for some promo or special sets. |
| `set_id` | string | Unique numeric identifier for the set (group_id) |
| `card_count` | number | Number of card **printings** in the set — one per TCGPlayer product, so alternate printings of the same collector number (Master Ball Pattern, Poké Ball Pattern, …) count separately. Equals the number of card entries in `/sets/:setCode`. Sealed products are **not** included — see `sealed_count`. |
| `number_count` | number | Number of distinct **collector numbers** in the set: `025/165` counts once however many printings it has. **Omitted** (not `0`) when not available. |
| `sealed_count` | number | Number of sealed products in the set. See [Product Types](https://pokewallet.io/api-docs#product-types). **Omitted** (not `0`) when not yet available for this deployment — a missing field means "unknown", not "none". |
| `language` | string \| null | Set language code (e.g., "eng", "jap", "ger"). May be `null` if not specified. |
| `release_date` | string \| null | Set release date (e.g., "3rd August, 2007"). May be `null` if unknown. |

### Example Request:

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets"
```

### Response Example:

*200 OK*

```json
{
  "success": true,
  "data": [
    {
      "name": "SV09: Journey Together",
      "set_code": "JTG",
      "set_id": "24073",
      "card_count": 190,
      "number_count": 190,
      "sealed_count": 27,
      "language": "eng",
      "release_date": "28th March, 2025"
    },
    {
      "name": "SV2a: Pokemon Card 151",
      "set_code": "SV2a",
      "set_id": "23599",
      "card_count": 516,
      "number_count": 210,
      "sealed_count": 4,
      "language": "jap",
      "release_date": "16th June, 2023"
    },
    {
      "name": "Plasma Freeze",
      "set_code": "PLF",
      "set_id": "1382",
      "card_count": 122,
      "number_count": 122,
      "sealed_count": 5,
      "language": "eng",
      "release_date": "8th May, 2013"
    }
  ],
  "total": 864
}
```

> `card_count + sealed_count` is the number of entries listed by `/sets/:setCode` (its `total_cards`). SV2a above has 210 collector numbers but 516 printings: most numbers exist as a base card plus Poké Ball Pattern and Master Ball Pattern versions, each with its own ID and price.

### GET /sets/:setCode

Get set details with paginated card list.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `setCode` | string | Required | Set identifier - accepts either `set_code` (e.g., "PLF", "SV2a") or `set_id` (e.g., "1382"). Both values are returned by the `/sets` endpoint. Prefer `set_id`: it is always unique. |
| `page` | number | Optional | Page number (default: 1) |
| `limit` | number | Optional | Results per page (default: 50, max: 200) |
| `language` | string | Optional | Filter by language when a `set_code` matches multiple sets (e.g., `eng`, `jap`, `chn`). Not needed when using a numeric `set_id`. |

### Set Object Fields:

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Set name |
| `set_code` | string \| null | Short code for the set. May be `null` for some promo sets. |
| `set_id` | string | Unique numeric identifier for the set (group_id) |
| `total_cards` | number | Total number of entries in the `cards` list, across all pages: every card printing **plus** sealed products. |
| `language` | string \| null | Set language code (e.g., "eng", "jap"). May be `null` if not specified. |
| `release_date` | string \| null | Set release date (e.g., "3rd August, 2007"). May be `null` if unknown. |

> The `cards` list has **one entry per printing**. Alternate printings of the same collector number — Master Ball Pattern, Poké Ball Pattern, Pokémon Center exclusives, Prerelease `[Staff]`, Cosmos Holofoil — are separate entries, each with its own `id` and prices. Sealed products are listed too: each entry's `card_info.product_type` is `"card"` or `"sealed"`. See [Product Types](https://pokewallet.io/api-docs#product-types).

### Card Fields Specific to This Endpoint:

| Field | Type | Description |
| --- | --- | --- |
| `card_info.variants` | string[] | TCGPlayer finishes of this printing, e.g. `["Normal", "Reverse Holofoil"]`. A reverse holo sold as the same product is a finish of the same entry, not a separate entry. Empty for CardMarket-only cards and sealed products. Only returned by `/sets/:setCode`. |

### Example Request:

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets/PLF"
```

### Response Example:

*200 OK*

```json
{
  "success": true,
    "set": {
        "name": "Plasma Freeze",
        "set_code": "PLF",
        "set_id": "1382",
        "total_cards": 127,
        "language": "eng",
        "release_date": "8th May, 2013"
    },
    "cards": [
        {
            "id": "pk_4687f4c4a0499fd6a7fa267a9bef2cb4c462f19478522c5a85eaf116ca1d1f0c8ad2ba5fc8ac3c668627bce5",
            "card_info": {
                "name": "Weedle",
                "clean_name": "Weedle",
                "set_name": "Plasma Freeze",
                "set_code": "1382",
                "card_number": "1/116",
                "rarity": "Common",
                "card_type": null,
                "product_type": "card",
                "variants": ["Normal", "Reverse Holofoil"],
                "hp": "50.0",
                "stage": "Basic",
                "card_text": null,
                "attacks": [
                    "[G] Triple Stab (10x) Flip 3 coins.  This attack does 10 damage times the number of heads."
                ],
                "weakness": "Rx2",
                "resistance": null,
                "retreat_cost": "1.0"
            },
            "images": {
                "languages": ["en", "it", "fr", "de", "es", "pt"]
            },
            "tcgplayer": {
                "prices": [],
                "url": "https://www.tcgplayer.com/product/90547"
            },
            "cardmarket": {
                "product_name": "Weedle",
                "prices": [],
                "product_url": "https://www.cardmarket.com/en/Pokemon/Products/Singles/Plasma-Freeze/Weedle-PLF1"
            }
        },
        {
            "id": "pk_d5e4b0f70f10d85b1439e7169de2b226b8265aa41579d52defcdbf3361dc53898e5e143670fdbcd2",
            "card_info": {
                "name": "Plasma Freeze Booster Box",
                "clean_name": "Plasma Freeze Booster Box",
                "set_name": "Plasma Freeze",
                "set_code": "PLF",
                "card_number": null,
                "rarity": null,
                "card_type": null,
                "product_type": "sealed",
                "variants": [],
                "hp": null,
                "stage": null,
                "card_text": null,
                "attacks": [],
                "weakness": null,
                "resistance": null,
                "retreat_cost": null
            },
            "images": {
                "languages": ["en"]
            },
            "tcgplayer": {
                "prices": [],
                "url": "https://www.tcgplayer.com/product/98516"
            },
            "cardmarket": null
        }

```

### Alternate Printings:

The same collector number can appear several times, once per printing. Real example from SV2a (IDs shortened):

*SV2a — card number 025/165*

```json
[
  { "id": "pk_ca32920ee8a538e4794fa…", "card_info": { "name": "Pikachu - 025/165", "card_number": "025/165", "variants": ["Normal"] } },
  { "id": "pk_a2d13d65cfc4dfb669f2d…", "card_info": { "name": "Pikachu - 025/165 (Poke Ball Pattern)", "card_number": "025/165", "variants": ["Holofoil"] } },
  { "id": "pk_779e688ac75c524ebcdd6…", "card_info": { "name": "Pikachu - 025/165 (Master Ball Pattern)", "card_number": "025/165", "variants": ["Holofoil"] } }
]
```

> CardMarket does not distinguish these printings. When a collector number has several printings, the `cardmarket` block is attached to one of them — normally the base printing — and is `null` on the others. TCGPlayer prices are always the printing's own.

### Set Not Found:

*404 Not Found*

```json
{
  "error": "Set not found: invalid set_code"
}
```

### Disambiguation Response:

Some set codes (e.g., `"PR"`) are shared by multiple sets with different `set_id`s. When this happens, instead of returning a single set, the API returns a disambiguation response listing all matching sets so you can pick the right one.

*200 OK — Multiple matches*

```json
{
  "disambiguation": true,
  "message": "Multiple sets found for set_code 'PR'. Use set_id to specify which one.",
  "matches": [
    {
      "set_id": "1938",
      "set_code": "PR",
      "name": "Alternate Art Promos",
      "language": "eng",
      "release_date": null
    },
    {
      "set_id": "1451",
      "set_code": "PR",
      "name": "XY Promos",
      "language": "eng",
      "release_date": null
    },
    {
      "set_id": "1407",
      "set_code": "PR",
      "name": "Black and White Promos",
      "language": "eng",
      "release_date": null
    }
  ],
  "total": 21
}
```

> **Tip:** When you receive a disambiguation response, pick the `set_id` of the set you need and call `/sets/1938` (using the numeric `set_id`) to get that specific set with its cards.

### GET /sets/:setCode/image

Retrieve the logo/image for a set. Returns binary image data (PNG). Not all sets have an image available.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `setCode` | string | Required | Set identifier — accepts either `set_code` (e.g., `SWSH3`) or numeric `set_id` (e.g., `23456`). Both are returned by the `/sets` endpoint. |
| `language` | string | Optional | Required only when a `set_code` is shared by multiple sets (e.g., `eng`, `jap`). Not needed when using a numeric `set_id`. |

### Response Headers:

- `Content-Type`: `image/png`
- `Cache-Control`: `public, max-age=86400`

### Example Request:

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets/SWSH3/image" \
     --output swsh3.png
```

### Error Responses:

*404 — Image not available*

```json
{
  "error": "Image not available",
  "message": "No set image is available for this set."
}
```

*409 — Ambiguous set code*

```json
{
  "error": "Ambiguous set code",
  "message": "Multiple sets found for set_code 'SM11'. Add ?language=eng (or jap, etc.) to disambiguate, or use a numeric set_id.",
  "matches": [
    { "set_id": "2464", "language": "eng" },
    { "set_id": "23690", "language": "jap" }
  ]
}
```

> **Tip:** Use a numeric `set_id` (e.g., `/sets/23456/image`) to always get a unique result without disambiguation.

### GET /sets/:setCode/statistics (PRO)

Get complete price statistics and rarity breakdown for a set. Returns average, min and max prices from both TCGPlayer (USD) and CardMarket (EUR), plus the most expensive card and a 7-day trend direction.

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `setCode` | string | Required | Numeric group_id (`23599`) or alphanumeric set code (`sv2a`) |
| `variant` | string | Optional | Filter CardMarket prices by variant: `normal`, `holo`. Does not affect TCGPlayer data. |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets/23599/statistics"
```

### Response Example:

*200 OK*

```json
{
  "set_info": {
    "name": "SV2a_ Pokemon Card 151",
    "set_code": "23599",
    "total_cards": 524
  },
  "price_statistics": {
    "tcgplayer": {
      "avg_market_price": "7.80",
      "min_price": 0.07,
      "max_price": 307.56,
      "most_expensive": { "name": "Charizard ex - 201/165", "price": 307.56 }
    },
    "cardmarket": {
      "avg_price": "2.34",
      "min_price": 0.37,
      "max_price": 17.91,
      "trend_direction": "up"
    }
  },
  "rarity_breakdown": {
    "Common": 199,
    "Uncommon": 186,
    "Rare": 77,
    "Double Rare": 12,
    "Art Rare": 18,
    "Super Rare": 16,
    "Special Art Rare": 8,
    "Ultra Rare": 3,
    "Unknown": 5
  }
}
```

> **Sealed products excluded:** booster boxes, ETBs, displays and similar are left out of `price_statistics` and `rarity_breakdown` — they don't have a `rarity` and would otherwise dominate the price range. See [Product Types](https://pokewallet.io/api-docs#product-types).

### Trial Response Headers:

When accessing this endpoint during an active trial, two extra headers are included:

```
X-Trial-Expires-At: 2026-05-01T00:00:00.000Z
X-Trial-Days-Remaining: 7
```

### Error Responses:

| Status | Description |
| --- | --- |
| `403` — trial not activated | `{ "error": "Trial not activated", "redeem_url": "https://pokewallet.io/dashboard" }` |
| `403` — trial expired | `{ "error": "Pro plan required", "upgrade_url": "https://pokewallet.io/pricing" }` |

### GET /sets/trending (PRO)

Returns the sets with the highest average price variation over the last 7 or 30 days, based on CardMarket moving averages. Each set includes the top movers (individual cards with the largest price change).

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `period` | string | Optional | Time window: `7d` (default) or `30d` |
| `limit` | number | Optional | Number of sets to return (default: 10, max: 50) |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets/trending?period=7d&limit=5"
```

### Response Example:

*200 OK*

```json
{
  "period": "7d",
  "trending_sets": [
    {
      "set_name": "Miracle of the Desert",
      "set_code": "-4",
      "card_count": 46,
      "cardmarket": {
        "price_change": "+90.6%",
        "avg_price_current": 15.67,
        "avg_price_previous": 8.22,
        "top_movers": [
          { "name": "Dusclops (ADV2 031)", "change": "+449.1%" },
          { "name": "Raichu ex (ADV2 023)", "change": "+433.3%" },
          { "name": "Typhlosion ex (ADV2 013)", "change": "+389.4%" }
        ]
      },
      "tcgplayer": null
    },
    {
      "set_name": "BW1_ Black Collection",
      "set_code": "23893",
      "card_count": 18,
      "cardmarket": {
        "price_change": "+40.8%",
        "avg_price_current": 2.22,
        "avg_price_previous": 1.58,
        "top_movers": [
          { "name": "Beartic (BW1b 018)", "change": "+171.2%" },
          { "name": "Emboar (BW1b 010)", "change": "+97.4%" },
          { "name": "Sawsbuck (BW1b 007)", "change": "-86.3%" }
        ]
      },
      "tcgplayer": {
        "status": "work_in_progress",
        "message": "TCGPlayer price history not yet available"
      }
    }
  ]
}
```

### Trial Response Headers:

When accessing this endpoint during an active trial, two extra headers are included:

```
X-Trial-Expires-At: 2026-05-01T00:00:00.000Z
X-Trial-Days-Remaining: 7
```

### Error Responses:

| Status | Description |
| --- | --- |
| `403` — trial not activated | `{ "error": "Trial not activated", "redeem_url": "https://pokewallet.io/dashboard" }` |
| `403` — trial expired | `{ "error": "Pro plan required", "upgrade_url": "https://pokewallet.io/pricing" }` |

### GET /sets/:setCode/completion-value (PRO)

Calculates the estimated cost to complete a set by buying one copy of every card. Returns separate estimates for TCGPlayer (USD) and CardMarket (EUR) — no cross-currency conversion is performed. Includes a breakdown by rarity.

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `setCode` | string | Required | Numeric group_id (`23599`) or alphanumeric set code (`sv2a`). CardMarket-only sets (negative group_id) return 404. |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/sets/23599/completion-value"
```

### Response Example:

*200 OK*

```json
{
  "set_info": {
    "name": "SV2a_ Pokemon Card 151",
    "set_code": "23599",
    "total_cards": 524
  },
  "completion_cost": {
    "tcgplayer": {
      "currency": "USD",
      "low_estimate": 4516.16,
      "market_estimate": 4056.03,
      "high_estimate": 13687.29
    },
    "cardmarket": {
      "currency": "EUR",
      "low_estimate": 683.50,
      "avg_estimate": 1232.46,
      "trend_estimate": 1526.69
    },
    "note": "TCGPlayer prices in USD, CardMarket prices in EUR. Cross-currency comparison not available."
  },
  "breakdown_by_rarity": {
    "Common": { "count": 199, "total_cost": 950.32 },
    "Uncommon": { "count": 186, "total_cost": 732.37 },
    "Rare": { "count": 77, "total_cost": 645.50 },
    "Double Rare": { "count": 12, "total_cost": 8.29 },
    "Art Rare": { "count": 18, "total_cost": 254.25 },
    "Super Rare": { "count": 16, "total_cost": 85.38 },
    "Special Art Rare": { "count": 8, "total_cost": 893.35 },
    "Ultra Rare": { "count": 3, "total_cost": 31.02 },
    "Unknown": { "count": 5, "total_cost": 12.47 }
  }
}
```

> **Sealed products excluded:** the completion cost is for one copy of each card — a booster box or display isn't a card to collect, so it's left out of `completion_cost` and `breakdown_by_rarity`. See [Product Types](https://pokewallet.io/api-docs#product-types).

*404 — CardMarket-only set*

```json
{
  "success": false,
  "error": "This is a CardMarket-only set without TCGPlayer pricing data"
}
```

### Trial Response Headers:

When accessing this endpoint during an active trial, two extra headers are included:

```
X-Trial-Expires-At: 2026-05-01T00:00:00.000Z
X-Trial-Days-Remaining: 7
```

### Error Responses:

| Status | Description |
| --- | --- |
| `403` — trial not activated | `{ "error": "Trial not activated", "redeem_url": "https://pokewallet.io/dashboard" }` |
| `403` — trial expired | `{ "error": "Pro plan required", "upgrade_url": "https://pokewallet.io/pricing" }` |

## Prices Endpoints

### GET /prices/:setCode (PRO)

Returns prices for every card in a Pokémon set, flat list, all variants. Use `?source` to restrict to a single marketplace.

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Path Parameter:

| Parameter | Type | Examples |
| --- | --- | --- |
| `:setCode` | string or number | `MEW`, `sv2a` (set code, case-insensitive) \| `22873`, `-188` (numeric group_id) |

### Query Parameters:

| Parameter | Type | Required | Values |
| --- | --- | --- | --- |
| `source` | string | No | `tcg` — TCGPlayer only \| `cm` — CardMarket only \| omit for both |

### Example Requests:

*cURL*

```bash
# Both sources (default)
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/prices/MEW"

# TCGPlayer only
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/prices/MEW?source=tcg"

# By numeric group_id
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/prices/22873"
```

### Response Example:

*200 OK*

```json
{
  "success": true,
  "source": "both",
  "set_code": "MEW",
  "group_id": "23237",
  "total": 361,
  "data": [
    {
      "id": "pk_72046138a4c1908a9f27c93fdd8189ba4ac8e683efaed6b9161efcef129302394a9ec1d20d",
      "card_number": "001/165",
      "name": "Bulbasaur - 001/165",
      "variant": "Normal",
      "product_type": "card",
      "tcgplayer": {
        "low_price": 0.05,
        "mid_price": 0.20,
        "high_price": 2.00,
        "market_price": 0.15,
        "direct_low_price": 0.10,
        "updated_at": "2026-05-04T07:43:36.088579"
      },
      "cardmarket": {
        "avg": 0.10,
        "low": 0.04,
        "trend": 0.09,
        "avg1": 0.08,
        "avg7": 0.09,
        "avg30": 0.11,
        "updated_at": "2026-05-04T06:47:05.90041"
      }
    },
    {
      "id": "pk_72046138a4c1908a9f27c93fdd8189ba4ac8e683efaed6b9161efcef129302394a9ec1d20d",
      "card_number": "001/165",
      "name": "Bulbasaur - 001/165",
      "variant": "Reverse Holofoil",
      "product_type": "card",
      "tcgplayer": { "market_price": 0.30, ... },
      "cardmarket": null
    },
    {
      "id": "pk_9a1c2e7f4b0d8365a1f2e9c7b4a08d6e1f3c5a7b9d0e2f4a6c8b0d2e4f6a8c0b",
      "card_number": null,
      "name": "Mew Booster Box",
      "variant": "products",
      "product_type": "sealed",
      "tcgplayer": { "market_price": 145.0, ... },
      "cardmarket": null
    }
  ]
}
```

### Disambiguation (300):

If `:setCode` matches multiple sets (e.g. same code in different languages), a `300` is returned. Pass the numeric `group_id` directly to disambiguate.

*300 Multiple Choices*

```json
{
  "disambiguation": true,
  "message": "Multiple sets found for 'SVI'. Use the numeric group_id to specify which one.",
  "matches": [
    { "group_id": "22873", "set_code": "SVI" },
    { "group_id": "23822", "set_code": "svI" }
  ]
}
```

### Error Codes:

| HTTP | Condition |
| --- | --- |
| `400` | Invalid `source` value |
| `401` | Missing or invalid API key |
| `403` | Plan does not include this endpoint — upgrade to Pro or activate the 7-day trial |
| `404` | Set not found, or `source=tcg` on a CM-only set |

> For CM-only sets (`group_id < 0`): `tcgplayer: null` on all cards and `variant: null`. For Japanese sets, `cardmarket: null` is expected as CM only covers the EU market. Response is cached for 15 minutes. **Sealed Products:** sealed rows (`product_type: "sealed"`) stay in the list, priced via TCGPlayer only — `cardmarket` is always `null` for them, on both TCG and CM-only sets. See [Product Types](https://pokewallet.io/api-docs#product-types).

## Analytics Endpoints

> **Authentication Required:** All Analytics endpoints require an API key.

### GET /analytics/top-cards (Pro)

Get the top N cards ranked by current price or 7-day percentage growth, from TCGPlayer or CardMarket.

> **Pro feature — 7-day free trial available.** Activate your free trial from the [dashboard](https://pokewallet.io/dashboard) to access this endpoint at no cost.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `metric` | string | Optional | Ranking metric. One of `price` (default) or `growth` (7-day % change). Any other value returns 400. |
| `source` | string | Optional | Price source. One of `tcg` (default) or `cm`. Any other value returns 400. |
| `limit` | number | Optional | Number of results to return (default: 20, max: 100). |

### Example Request:

*cURL*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/analytics/top-cards?metric=growth&source=cm&limit=10"
```

### Response Example — `metric=price&source=tcg`:

*200 OK*

```json
{
  "metric": "price",
  "source": "tcg",
  "top_cards": [
    {
      "id": "pk_abc123",
      "card_info": {
        "name": "Charizard VSTAR",
        "set_name": "Brilliant Stars",
        "card_number": "018/172",
        "rarity": "Ultra Rare"
      },
      "current_price": 42.50
    }
  ]
}
```

### Response Example — `metric=growth&source=cm`:

*200 OK*

```json
{
  "metric": "growth",
  "source": "cm",
  "top_cards": [
    {
      "id": "pk_def456",
      "card_info": {
        "name": "Charizard ex (SV3 125)",
        "set_code": "SV3",
        "card_number": "125"
      },
      "current_price": 18.50,
      "change_7d": "+54.2%"
    }
  ]
}
```

> `metric=growth` ranks by 7-day change. For `source=tcg` uses `price_7d` snapshots — cards without 7-day history are excluded. For `source=cm` uses CardMarket's native `avg7`.

> **Sealed products excluded:** booster boxes, ETBs and displays are left out of both rankings — they'd otherwise dominate by price. See [Product Types](https://pokewallet.io/api-docs#product-types).

### Trial Response Headers:

When accessing this endpoint during an active trial, two extra headers are included:

```
X-Trial-Expires-At: 2026-05-01T00:00:00.000Z
X-Trial-Days-Remaining: 7
```

### Error Responses:

| Status | Description |
| --- | --- |
| `403` — trial not activated | `{ "error": "Trial not activated", "redeem_url": "https://pokewallet.io/dashboard" }` |
| `403` — trial expired | `{ "error": "Pro plan required", "upgrade_url": "https://pokewallet.io/pricing" }` |

## Images Endpoint

> **Authentication Required:** The Images endpoint requires an API key.

### GET /images/:id

Retrieve card images by card ID with automatic fallback system. Returns binary image data (JPEG, or WebP for localized images). Supports both TCG card IDs (`pk_xxx`) and CardMarket-only card IDs (no prefix). Localized card artwork is available in 5 European languages via the `lang` parameter.

### Parameters:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Required | Card ID - Two formats supported: **TCG cards:** `pk_xxx` (with prefix); **CardMarket-only:** hexadecimal hash (no prefix) Obtained from `/search`, `/cards/:id`, or `/sets/:setCode` |
| `size` | string | Optional | Image size: `low` (~500px, ~50KB) or `high` (~1000px, ~200KB). Default: `low` |
| `lang` | string | Optional | Localized image language: `it`, `fr`, `de`, `es`, or `pt`. If a localized version exists, it is served as WebP; otherwise the request silently falls back to the card's default image (never a 404 because of the language). Check the card's `images.languages` field to know which languages are available. |

### Response Headers:

- `Content-Type`: `image/jpeg`, or `image/webp` (localized images)
- `X-Image-Lang`: language code of the served image (only present on localized responses, e.g. `it`)
- `Cache-Control`: `public, max-age=31536000, immutable`

### Localized Images (Multi-Language):

By default, each card is served with its original artwork: **English** for international sets, **Japanese** for Japanese sets. For English-language cards, localized European variants may also be available where a translated print exists: Italian (`it`), French (`fr`), German (`de`), Spanish (`es`) and Portuguese (`pt`).

The recommended flow:

1. Fetch the card from `/cards/:id`, `/search`, or `/sets/:setCode` and read its `images.languages` array (e.g. `["en", "it", "de"]`).
2. If it contains your user's language, request `/images/:id?lang=xx` to get the localized WebP.
3. If it doesn't (or the localized file is missing), the API serves the card's default image (English or Japanese) — requests with `lang` are always safe.

> **Backward Compatible:** requests without `lang` behave exactly as before. The `X-Image-Lang` header tells you when a localized WebP was served; its absence means the card's default image (English or Japanese) was returned.

### Example Requests:

#### TCG Card (with pk_ prefix):

*High Resolution TCG Card*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/images/pk_72046138a4c1908a9f27c93fdd8189ba4ac8e683efaed6b9161efcef129302394a9ec1d20d?size=high" \
     --output pikachu_tcg.jpg
```

#### Localized Card Image (Italian):

*Localized Card Image*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/images/pk_72046138a4c1908a9f27c93fdd8189ba4ac8e683efaed6b9161efcef129302394a9ec1d20d?size=high&lang=it" \
     --output pikachu_it.webp
```

#### CardMarket-Only Card (no prefix):

*CardMarket-Only Card Image*

```bash
curl -H "X-API-Key: pk_live_your_key_here" \
     "https://api.pokewallet.io/images/8eb728bd59d4e1a0d94c354e4cc65a7465b55cfa4261bfa93301faa5ec921f97?size=high" \
     --output cubone_v1.jpg
```

> **Unique IDs for Variants:** Each CardMarket variant (V1, V2, etc.) has its own unique ID, so you don't need to use a `set` parameter for disambiguation. The ID itself uniquely identifies the exact card variant you want.

### Example Response (Headers):

```
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 123456
Cache-Control: public, max-age=31536000, immutable

[Binary image data]
```

### Example Response (Localized, Headers):

```
HTTP/1.1 200 OK
Content-Type: image/webp
Content-Length: 98765
X-Image-Lang: it
Cache-Control: public, max-age=31536000, immutable

[Binary image data]
```

## Code Examples

*JavaScript (Fetch)*

```javascript
const API_KEY = process.env.POKEWALLET_API_KEY;
const BASE_URL = 'https://api.pokewallet.io';

async function searchCards(query) {
  const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}`, {
    headers: {
      'X-API-Key': API_KEY
    }
  });

  if (!response.ok) {
    if (response.status === 429) {
      throw new Error('Rate limit exceeded');
    }
    throw new Error(`API error: ${response.status}`);
  }

  const data = await response.json();
  return data;
}

async function getCardImage(cardId, size = 'high') {
  const url = `${BASE_URL}/images/${cardId}?size=${size}`;

  const response = await fetch(url, {
    headers: {
      'X-API-Key': API_KEY
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch image: ${response.status}`);
  }

  // Return blob for display or download
  const blob = await response.blob();
  return blob;
}

// Usage - Search TCG cards
searchCards('pikachu')
  .then(data => {
    console.log('TCG card results:', data);
    // IDs will have pk_ prefix
  })
  .catch(error => console.error(error));

// Usage - Search CardMarket-only cards
searchCards('cubone cbb3c')
  .then(data => {
    console.log('CardMarket-only results:', data);
    // IDs will NOT have pk_ prefix
  })
  .catch(error => console.error(error));

// Usage - Get Image (TCG card with pk_ prefix)
getCardImage('pk_72046138a4c1908a9f27c93fdd8189ba...', 'high')
  .then(blob => {
    const imageUrl = URL.createObjectURL(blob);
    document.querySelector('img').src = imageUrl;
  })
  .catch(error => console.error(error));

// Usage - Get Image (CardMarket-only, no prefix)
getCardImage('8eb728bd59d4e1a0d94c354e4cc65a74...', 'high')
  .then(blob => {
    const imageUrl = URL.createObjectURL(blob);
    document.querySelector('img').src = imageUrl;
  })
  .catch(error => console.error(error));
```

*Python (Requests)*

```python
import os
import requests

API_KEY = os.getenv('POKEWALLET_API_KEY')
BASE_URL = 'https://api.pokewallet.io'

def search_cards(query):
    headers = {
        'X-API-Key': API_KEY
    }

    response = requests.get(
        f'{BASE_URL}/search',
        params={'q': query},
        headers=headers
    )

    if response.status_code == 429:
        raise Exception('Rate limit exceeded')

    response.raise_for_status()
    return response.json()

def get_card_image(card_id, size='high', set_code=None):
    headers = {
        'X-API-Key': API_KEY
    }

    params = {'size': size}
    if set_code:
        params['set'] = set_code

    response = requests.get(
        f'{BASE_URL}/images/{card_id}',
        params=params,
        headers=headers
    )

    if response.status_code == 429:
        raise Exception('Rate limit exceeded')

    response.raise_for_status()

    return response.content

# Usage - Search
try:
    data = search_cards('charizard ex')
    print(data)
except Exception as e:
    print(f'Error: {e}')

# Usage - Get Image
try:
    image_data = get_card_image('pk_xxx', size='high', set_code='CBB3C')

    # Save to file
    with open('charizard.jpg', 'wb') as f:
        f.write(image_data)

    print('Image downloaded successfully')
except Exception as e:
    print(f'Error: {e}')
```

*Node.js (Axios)*

```javascript
const axios = require('axios');
const fs = require('fs');

const API_KEY = process.env.POKEWALLET_API_KEY;
const BASE_URL = 'https://api.pokewallet.io';

async function searchCards(query) {
  try {
    const response = await axios.get(`${BASE_URL}/search`, {
      params: { q: query },
      headers: {
        'X-API-Key': API_KEY
      }
    });

    return response.data;
  } catch (error) {
    if (error.response?.status === 429) {
      throw new Error('Rate limit exceeded');
    }
    throw error;
  }
}

async function getCardImage(cardId, size = 'high', setCode = null) {
  try {
    const params = { size };
    if (setCode) {
      params.set = setCode;
    }

    const response = await axios.get(`${BASE_URL}/images/${cardId}`, {
      params,
      headers: {
        'X-API-Key': API_KEY
      },
      responseType: 'arraybuffer'
    });

    // Get image source
    const imageSource = response.headers['x-image-source'];
    console.log(`Image served from: ${imageSource}`);

    return response.data;
  } catch (error) {
    if (error.response?.status === 429) {
      throw new Error('Rate limit exceeded');
    }
    throw error;
  }
}

// Usage - Search
searchCards('charizard ex')
  .then(data => console.log(data))
  .catch(error => console.error(error));

// Usage - Get Image
getCardImage('pk_xxx', 'high', 'CBB3C')
  .then(imageBuffer => {
    fs.writeFileSync('charizard.jpg', imageBuffer);
    console.log('Image downloaded successfully');
  })
  .catch(error => console.error(error));
```

*Go*

```go
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
)

const BaseURL = "https://api.pokewallet.io"

func searchCards(query string) (map[string]interface{}, error) {
    apiKey := os.Getenv("POKEWALLET_API_KEY")

    params := url.Values{}
    params.Add("q", query)

    req, err := http.NewRequest("GET", BaseURL+"/search?"+params.Encode(), nil)
    if err != nil {
        return nil, err
    }

    req.Header.Set("X-API-Key", apiKey)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode == 429 {
        return nil, fmt.Errorf("rate limit exceeded")
    }

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    var result map[string]interface{}
    json.Unmarshal(body, &result)

    return result, nil
}

func getCardImage(cardID, size, setCode string) ([]byte, error) {
    apiKey := os.Getenv("POKEWALLET_API_KEY")

    params := url.Values{}
    params.Add("size", size)
    if setCode != "" {
        params.Add("set", setCode)
    }

    req, err := http.NewRequest("GET", BaseURL+"/images/"+cardID+"?"+params.Encode(), nil)
    if err != nil {
        return nil, err
    }

    req.Header.Set("X-API-Key", apiKey)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode == 429 {
        return nil, fmt.Errorf("rate limit exceeded")
    }

    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("failed to fetch image: %d", resp.StatusCode)
    }

    imageData, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    return imageData, nil
}

func main() {
    // Search example
    data, err := searchCards("charizard ex")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(data)

    // Image example
    imageData, err := getCardImage("pk_xxx", "high", "CBB3C")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // Save to file
    err = os.WriteFile("charizard.jpg", imageData, 0644)
    if err != nil {
        fmt.Println("Error saving image:", err)
        return
    }

    fmt.Println("Image downloaded successfully")
}
```

## Error Handling

The API uses standard HTTP status codes to indicate success or failure:

- **200 OK** — Request successful
- **400 Bad Request** — Invalid parameters or malformed request
- **401 Unauthorized** — Missing or invalid API key
- **403 Forbidden** — Paid endpoint without the required plan or an active trial: Pro for Pokémon and One Piece, Ultra for the other games. The body includes `redeem_url` (trial not activated yet) or `upgrade_url` (trial expired).
- **404 Not Found** — Card, set or image does not exist
- **405 Method Not Allowed** — The API only accepts `GET` requests.
- **409 Conflict** — `/sets/:setCode/image`: the set code matches several sets. Retry with `?language=` or the numeric `set_id`.
- **429 Too Many Requests** — Rate limit exceeded
- **500 Internal Server Error** — Something went wrong on our side
- **502 Bad Gateway** — An image could not be fetched from its source. Temporary: retry later.

### Errors on game endpoints (`/mtg`, `/ygo`, …)

| Status | `error` | When |
| --- | --- | --- |
| 400 | `Invalid or corrupted card ID` | ID is neither `<game>_…` nor 64 hex, or belongs to another game |
| 400 | `Query parameter required` | `q` missing, shorter than 2 or longer than 100 characters |
| 400 | `Search query rejected` | `q` contains SQL-like text: rephrase it |
| 400 | `Invalid or corrupted card ID` + `did_you_mean` | The ID belongs to another game (e.g. a `lor_…` ID on `/mtg/cards`): `did_you_mean` has the right path |
| 404 | `Set not found` + `suggestions` | No set with that `set_id`/`set_code`. If you wrote the set *name*, `suggestions` lists the matching sets with their `set_id` |
| 404 | `Card not found`, `Image not found` | Card or image does not exist |
| 502 | `Image not available` | Image source temporarily unreachable |
| 503 | `Search temporarily unavailable` | The search took too long under load. Retry after the seconds in the `Retry-After` header, or use more specific words |

### Error Response Format

Every error has `error` (short, stable) and `message` (what to fix). When the API can tell what you meant, `did_you_mean` contains the corrected path:

```
// GET /magic/sets
  {
    "error": "Endpoint not found",
    "message": "Did you mean /mtg/sets?",
    "did_you_mean": "/mtg/sets",
    "docs": "https://pokewallet.io/api-docs"
  }
```

Authentication errors (401) also include an `example` request. A trailing slash is ignored: `/sets/` and `/sets` are the same endpoint.

## Best Practices

### Use Environment Variables

Never hardcode API keys. Always use environment variables or secure secret management.

### Handle Rate Limits Gracefully

Implement exponential backoff when you receive a 429 response. Check rate limit headers to avoid hitting limits.

### Cache Results

Cache API responses when appropriate to reduce requests and improve performance.

### Use Search Endpoint

Always use `/search` for the most complete and accurate data.

### Monitor Your Usage

Check your dashboard regularly to track API usage and avoid unexpected rate limit hits.

### Error Handling

Always implement proper error handling for network failures, rate limits, and invalid responses.

> **Security Reminder:** Keep your API keys secure. If you suspect a key has been compromised, revoke it immediately from your dashboard and generate a new one.

## Pokémon TCG API FAQ

### Is the Pokémon TCG API free?

Yes. The free tier (100 requests/hour, 1,000/day) covers sets, set cards, card details, search and images, with TCGPlayer and CardMarket prices. Price history, prices by set, set statistics, completion value, trending sets and top cards need the Pro plan (€20/month); a 7-day free trial starts from the dashboard.

### Does the Pokémon TCG API include CardMarket prices?

Yes. Cards return TCGPlayer (USD) prices for each variant (Normal, Holofoil, Reverse Holofoil, 1st Edition…) and CardMarket (EUR) prices for normal and holo. Cards sold only on CardMarket, such as Japanese sets and European promos, have an id without the pk_ prefix.

### Can I get the price history of a Pokémon card?

Yes. GET /cards/:id/price-history returns TCGPlayer and CardMarket price snapshots with the percentage change over 7, 14, 30, 60 and 120 days. It needs the Pro plan (€20/month) or its 7-day trial.

### Can I use the same API key for other games?

Yes. One PokéWallet API key works for all 12 games, Pokémon and One Piece included, and the rate limits are shared across them.

## Need Help?

If you have questions or run into issues, we're here to help!

[Go to Dashboard](https://pokewallet.io/dashboard)

[Contact Support](mailto:hello@pokewallet.io)

[Join Discord](https://discord.gg/znDxEjmDVP)
