Back to Database

Public API

v1.0.0 · Free · No signup required for 99% of endpoints

A free REST API for the full Hearthstone Battlegrounds card database. Built for bots, stat trackers, wiki templates, Discord integrations, Twitch extensions, or any tool that wants structured card data without scraping HTML. No signup, no OAuth, no credit card — paste a curl command and go.

30-second start

Paste into your terminal. Works right now, no setup:

curl https://hsbg.cards/api/v1/cards/sellemental

You get full card data as JSON. Look up cards by numeric id (64038), slug (64038-sellemental), or normalized name (sellemental, Amber Guardian). That's the entire shape of the API: hit an endpoint, get JSON back. Everything else is variations on that theme.

“I want to…”

Task-oriented recipes. Each one is a complete working example — click to expand the one that matches what you're building.

…build autocomplete card search (as the user types)

Endpoint: GET /api/v1/cards/suggest?q=<query>

This is the only endpoint that requires an API key. It's free — scroll down to “Request an API key” to get one.

Basic call:

curl -H "X-API-Key: hsbg_live_..." \
  "https://hsbg.cards/api/v1/cards/suggest?q=mur&limit=5"

Response:

{
  "query": "mur",
  "locale": "en_US",
  "pool": "current",
  "count": 2,
  "suggestions": [
    {
      "id": 41245,
      "slug": "41245-murloc-tidehunter",
      "name": "Murloc Tidehunter",
      "tier": 1,
      "cardType": "minion",
      "minionType": "Murloc",
      "image": "/api/v1/cards/41245/image?size=small",
      "ids": { "external": "CS2_168", "dbf": 40999, "dbfGold": 41000 },
      "matchType": "word-start"
    }
  ]
}

Each suggestion comes with a ready-to-use image URL — drop it straight into an <img> tag and the browser follows the 302 automatically.

Complete JS integration:

async function searchCards(query) {
  const res = await fetch(
    `https://hsbg.cards/api/v1/cards/suggest?q=${encodeURIComponent(query)}&limit=10`,
    { headers: { "X-API-Key": "hsbg_live_..." } }
  );
  const { suggestions } = await res.json();
  return suggestions;
}

// Render in your dropdown:
for (const s of suggestions) {
  row.innerHTML = `
    <img src="https://hsbg.cards${s.image}" alt="${s.name}">
    <span>${s.name}</span>
    <span>Tier ${s.tier}</span>
  `;
}

Query parameters:

  • q (required) — the user's typed query. Max 64 chars.
  • limit — max results. Default 10, capped at 25.
  • poolcurrent (default), legacy, upcoming, or all.
  • types — comma-separated card types to restrict results, e.g. minion,spell.

How ranking works — the matchType field:

  1. exact — normalized name equals the query (score 1000)
  2. prefix — name starts with the query (800) — e.g. mant → “Mantid King”
  3. word-start — any word of the name starts with the query (600)
  4. substring — query appears somewhere inside the name (400)
  5. fuzzy — Levenshtein distance typo tolerance (150, adjusted). Only for queries of 4+ chars. Includes a distance field.

⚠ Debounce your client at 150–250ms. Autocomplete fires on every keystroke. A careless client burns an entire per-minute quota in 5 seconds. This is the single most common integration mistake.

…look up one card by id, slug, or name

Endpoint: GET /api/v1/cards/{identifier}

No key required. The identifier can be any of:

  • Numeric id: 64038
  • Slug: 64038-sellemental
  • Normalized name: sellemental
  • URL-encoded display name: Amber%20Guardian

Example:

curl https://hsbg.cards/api/v1/cards/sellemental

Response (abbreviated):

{
  "data": {
    "id": 64038,
    "slug": "64038-sellemental",
    "name": "Sellemental",
    "image": "/cards/production/pngs/full/minions/all/64038-sellemental.png",
    "imageGold": "/cards/production/pngs/full/minions/all/64038-sellemental_golden.png",
    "text": "When you sell this, get a 3/3 Elemental.",
    "attack": 2,
    "attackGold": 4,
    "health": 2,
    "healthGold": 4,
    "tier": 2,
    "cardType": "minion",
    "minionTypes": ["Elemental"],
    "keywords": [],
    "pool": true,
    "externalId": "BGS_115",
    "dbfIdGold": 64041
  }
}

Just want one field? Use /field/:name:

curl https://hsbg.cards/api/v1/cards/sellemental/field/text
# When you sell this, get a 3/3 Elemental.

curl https://hsbg.cards/api/v1/cards/sellemental/field/atk
# 2

Scalar fields come back as plain text (no JSON parsing needed). Array fields come back as JSON. Friendly aliases work — atk, attack, and ATK all mean the same thing.

Want specific fields only? Pass ?fields=id,name,text,atk,hp to trim the response.

Typos get a helpful hint in the error response:

{
  "error": {
    "code": "card_not_found",
    "message": "No card matches 'selimental'",
    "hint": "Did you mean: 64038-sellemental?"
  }
}
…look up many cards at once (batch lookup)

Endpoints: GET /api/v1/cards?identifiers=a,b,c (up to ~20, URL-friendly) or POST /api/v1/cards/batch (up to 100, for larger batches).

No key required. Counts as 1 request toward your rate limit regardless of how many cards you ask for.

Via GET (simple):

curl "https://hsbg.cards/api/v1/cards?identifiers=sellemental,95261,amber-guardian&fields=id,name,text,atk,hp"

Via POST (for larger batches):

curl -X POST https://hsbg.cards/api/v1/cards/batch \
  -H "Content-Type: application/json" \
  -d '{
    "identifiers": ["sellemental", "95261", "amber-guardian"],
    "fields": ["id", "name", "text", "atk", "hp"],
    "pool": "all"
  }'

Response shape (both endpoints):

{
  "data": [
    { "id": 64038, "name": "Sellemental", "text": "..." },
    { "id": 95261, "name": "Eternal Knight", "text": "..." }
  ],
  "notFound": ["typo-identifier"],
  "disambiguation": {
    "sellemental": [
      { "id": 64038, "slug": "64038-sellemental", "name": "Sellemental" }
    ]
  }
}
  • data — cards that resolved cleanly
  • notFound — identifiers that matched nothing (this is not an error — you still get a 200)
  • disambiguation — identifiers that matched multiple cards; each lists the candidates

Unknown identifiers don't fail the request — they show up in notFound. Only structural errors (malformed JSON, missing identifiers, more than 100 items) return 4xx.

…display card images on my site

Endpoint: GET /api/v1/cards/{id}/image

No key required. Returns a 302 redirect to a real static file on disk — your browser follows it automatically, your CDN can cache the final URL.

Three sizes, two formats:

  • size=small — 150px height WebP, ~5 KB. Perfect for autocomplete dropdowns and list rows.
  • size=medium — 300px wide WebP, ~22 KB. Perfect for grid views and hover previews. This is the default.
  • size=full — source 512×673 WebP (~57 KB) or PNG (~420 KB). For modal / detail views and downloads.

PNG is only available at size=full. Requesting format=png at any other size returns 400 invalid_image_variant. Use format=webp (or leave it off — it's the default) for the other sizes.

Just drop the URL into an <img> tag:

<img src="https://hsbg.cards/api/v1/cards/sellemental/image?size=medium"
     alt="Sellemental">

Or curl examples:

# Default: medium webp for grid display
curl -L "https://hsbg.cards/api/v1/cards/sellemental/image" -o card.webp

# Tiny thumbnail for an autocomplete row
curl -L "https://hsbg.cards/api/v1/cards/sellemental/image?size=small" -o thumb.webp

# Full-size original PNG for a high-res download
curl -L "https://hsbg.cards/api/v1/cards/sellemental/image?size=full&format=png" -o card.png

# Golden (premium) variant
curl -L "https://hsbg.cards/api/v1/cards/sellemental/image?golden=true" -o card-gold.webp

Graceful fallbacks:

  • If you request golden=true but the card has no golden art, the endpoint serves the non-golden version and sets an X-Image-Fallback: no-golden response header so you can warn the user.
  • If you ask for format=png at any size other than full, you get 400 invalid_image_variant — the API is strict about this to prevent surprise re-encoding.
…get related cards (buddies, hero powers, tokens, skins)

Endpoint: GET /api/v1/cards/{id}/related

No key required. Returns a resolved view of everything related to the card — its parent, children, companion (for heroes), skins, and any cards mentioned in its text.

Example:

curl https://hsbg.cards/api/v1/cards/57944-a-f-kay/related

Response:

{
  "data": {
    "parent": null,
    "children": [
      { "id": 57945, "name": "A. F. Kay's Hero Power", "cardType": "hero_power" }
    ],
    "companion": { "id": 104538, "name": "Friendly Saloonkeeper" },
    "skins": [
      { "id": 57944001, "name": "Skin: A. F. Kay (Pirate)" }
    ],
    "textMentions": []
  }
}

Different card types have different related fields:

  • Heroes — have children (hero powers + buddies), maybe a companion (Duos partner), and skins (cosmetic portraits)
  • Minions — may have a parent (if they're a token), children (tokens they generate), and textMentions (cards referenced in their text)
  • Hero powers — have a parent (the hero)

If you only want hero skins specifically, there's a dedicated endpoint:

curl https://hsbg.cards/api/v1/cards/57944-a-f-kay/skins
…see a card's stats at a specific historical patch

Endpoint: GET /api/v1/cards/{id}/at/{patch}

No key required. Retrieves the card's state as it existed at a given tracked patch. Useful for “what did this look like before the last nerf?” or building historical analytics.

Example:

curl https://hsbg.cards/api/v1/cards/130084/at/35.0.3

Response:

{
  "data": {
    "patch": "35.0.3",
    "card": {
      "id": 130084,
      "name": "Demon Fodder",
      "slug": "130084-demon-fodder"
    },
    "snapshot": {
      "name": "Demon Fodder",
      "attack": 1,
      "health": 1,
      "text": "..."
    },
    "source": "patch_notes_snapshot",
    "changeType": "changed"
  }
}

The source field tells you how confident the snapshot is:

  • current_pool — the requested patch is the live patch. Snapshot is the live card. Fully accurate.
  • patch_notes_snapshot — reconstructed from an actual patch-notes entry. Most accurate for historical states.
  • reconstructed — the card wasn't directly changed in that patch. Best-effort fallback; may include a warning field.

Related: full change history for one card:

curl https://hsbg.cards/api/v1/cards/130084/history

Returns every patch-notes entry that ever mentioned this card.

…read patch-notes data

Endpoints:

  • GET /api/v1/patches — list all tracked patches with summaries
  • GET /api/v1/patches/{version} — full patch-notes data
  • GET /api/v1/patches/latest — 302 redirect to the newest patch

No key required.

Version accepts either the latest version alone (35.2) or the prev_curr pair (35.0.3_35.2). Both resolve to the same data:

curl https://hsbg.cards/api/v1/patches/35.2
curl https://hsbg.cards/api/v1/patches/35.0.3_35.2

Use latest if you want the newest patch without hardcoding the number:

curl -L https://hsbg.cards/api/v1/patches/latest
…grab the entire card database (bulk download)

Endpoint: GET /api/v1/cards with pagination

No key required.

The list endpoint supports filter + pagination:

# First page of current-pool minions
curl "https://hsbg.cards/api/v1/cards?pool=current&cardType=minion&limit=100&offset=0"

# Next page
curl "https://hsbg.cards/api/v1/cards?pool=current&cardType=minion&limit=100&offset=100"

Filter parameters:

  • poolcurrent, legacy, upcoming, or all
  • cardTypeminion, hero, spell, trinket, etc.
  • tier17 (tavern tier)
  • tribebeast, demon, dragon, etc.
  • limit — page size (1–200)
  • offset — page offset
  • fields — trim response to specific fields, e.g. id,name,text,atk

The response includes a total count and a hasMore flag so you know when to stop paginating.

Authentication

Short answer: almost never. 99% of endpoints are anonymous-accessible. The only endpoint that requires an API key is GET /api/v1/cards/suggest (autocomplete). Everything else — single lookup, batch, images, related, patches, history — works without authentication.

Why is suggest key-gated?

It's an autocomplete endpoint with very tight latency requirements and aggressive typo-tolerance. Serving it anonymously would let any visitor burn the shared rate-limit pool and degrade performance for partner integrations that actually depend on it.

All other endpoints are rate-limited per IP, which is a softer constraint — anonymous clients can still do a lot before hitting the cap.

How to use a key once you have one

Keys look like hsbg_live_<32 hex chars>. Pass via either:

# Preferred: header
curl -H "X-API-Key: hsbg_live_..." \
  "https://hsbg.cards/api/v1/cards/suggest?q=mur"

# Or: query param
curl "https://hsbg.cards/api/v1/cards/suggest?q=mur&api_key=hsbg_live_..."

Keep the key secret. Anyone with your key can exhaust your rate-limit budget. If it leaks, ask for a rotation — the API reloads keys from disk within 60 seconds, no restart required.

Rate limits

  • Without a key: 120 requests per minute, per IP. No signup, no catch.
  • With a key: configurable higher limits (default 600/min, partner tier up to 3000/min).
How to read the limit headers and handle 429s

Every response — success or error — carries these headers:

  • X-RateLimit-Limit — total requests allowed in the current minute
  • X-RateLimit-Remaining — how many you have left in this window
  • X-RateLimit-Reset — Unix timestamp (seconds) when the window resets
  • Retry-Afteronly on 429 responses — how many seconds to wait before retrying

Example of a healthy response:

HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1744632180
Content-Type: application/json

When you hit the limit:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Please wait 42 seconds before retrying.",
    "hint": "See X-RateLimit-Reset header for window reset timestamp."
  }
}

With Retry-After: 42 in the headers. Good clients respect Retry-After.

Practical tips:

  • Cache aggressively. Card data almost never changes. A 1-hour client cache is safe.
  • Debounce autocomplete at 150–250ms.
  • Prefer batch endpoints when looking up more than 2–3 cards — one batch request = one rate-limit unit.
  • Use field filtering (?fields=id,name,image) to shrink payloads.

Request an API key

Keys are free — the gate exists to manage load, not to monetize. Drop me a line on any of these. Tell me what you're building and I'll get you set up:

Advanced topics

Friendly field aliases (atk vs attack vs ATK)

Every ?fields= query param and every /field/:field path param accepts friendly aliases. The normalizer strips non-alphanumeric characters and lowercases the string — so Attack, attack, ATK, attack-gold, and atk_gold all resolve to the same canonical field.

AliasCanonical field
texttext
textgold, text_gold, goldentexttextGold
atk, attack, powerattack
atkgold, atk_gold, goldenatkattackGold
hp, health, lifehealth
hpgold, hp_gold, goldenhphealthGold
mana, cost, manacost, goldmanaCost
armor, armourarmor
tier, leveltier
flavor, flavortext, flavourflavorText
tribe, tribes, race, minionTypesminionTypes
kw, keywordskeywords
childrenchildIds
parentparentId
companioncompanionId
skinsskinIds
mentions, textmentionstextMentionIds

Canonical camelCase field names are always accepted verbatim.

Error format and common codes

All non-2xx responses follow the same shape:

{
  "error": {
    "code": "card_not_found",
    "message": "No card matches 'selimental'",
    "hint": "Did you mean: 64038-sellemental?"
  }
}
  • code — machine-readable, stable across versions
  • message — human-readable summary
  • hint — optional suggestion for how to fix it (e.g. typo correction)

Common codes:

CodeStatusMeaning
card_not_found404No card matches the identifier
field_not_found404Unknown field name
patch_not_found404Unknown patch version
skin_not_found404Unknown skin id
image_not_available404Card has no image
bad_request400Invalid or missing parameter
batch_too_large400Batch > 100 identifiers
invalid_image_variant400Unsupported size/format combo
api_key_required401Missing or invalid key on suggest
rate_limit_exceeded429Rate limit exceeded — see Retry-After
internal_error500Unexpected server-side error

Note on batch endpoints: if you batch 10 identifiers and 3 don't exist, you still get 200 OK with those 3 listed in notFound. Only structural errors return 4xx.

CORS & browser usage

/api/v1/* is fully open for cross-origin requests:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-API-Key

You can fetch() from any origin without a proxy. This includes file:// pages, localhost dev servers, Twitch extensions, Discord activities, and plain HTML files.

Note: the internal /api/cards endpoint (used by the website itself) has a narrower CORS policy — only *.ext-twitch.tv origins plus localhost dev. If you're building something external, always use /api/v1/*, not /api/cards.

Versioning and stability policy

/api/v1/* is a frozen contract. Additive changes (new endpoints, new fields, new query parameters) ship without a bump. Breaking changes will ship as /api/v2/* and v1 will keep working during a generous deprecation window.

Versioning follows semver. Current version is reported via the X-API-Version response header.

The internal /api/cards endpoint (used by the website itself) is not part of the public contract and can change without notice. Don't build against it.

Safe (additive) changes:

  • Adding new optional fields to responses
  • Adding new optional query parameters
  • Adding new endpoints
  • Relaxing validation

Breaking changes (would ship as v2):

  • Removing endpoints, fields, or query parameters
  • Changing the type or meaning of an existing field
  • Tightening validation on previously-accepted inputs
  • Adding a new required parameter on an existing endpoint

See also

Privacy Policy · Terms of Use

Full interactive reference

Every endpoint is documented below with request parameters, response schemas, and a “Try it” panel that fires real requests against the live API. Pick an endpoint, fill in the fields, hit send.

Open API Reference