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.
Paste into your terminal. Works right now, no setup:
curl https://hsbg.cards/api/v1/cards/sellementalYou 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.
Task-oriented recipes. Each one is a complete working example — click to expand the one that matches what you're building.
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.pool — current (default), legacy, upcoming, or all.types — comma-separated card types to restrict results, e.g. minion,spell.How ranking works — the matchType field:
exact — normalized name equals the query (score 1000)prefix — name starts with the query (800) — e.g. mant → “Mantid King”word-start — any word of the name starts with the query (600)substring — query appears somewhere inside the name (400)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.
Endpoint: GET /api/v1/cards/{identifier}
No key required. The identifier can be any of:
6403864038-sellementalsellementalAmber%20GuardianExample:
curl https://hsbg.cards/api/v1/cards/sellementalResponse (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
# 2Scalar 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?"
}
}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 cleanlynotFound — identifiers that matched nothing (this is not an error — you still get a 200)disambiguation — identifiers that matched multiple cards; each lists the candidatesUnknown identifiers don't fail the request — they show up in notFound. Only structural errors (malformed JSON, missing identifiers, more than 100 items) return 4xx.
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.webpGraceful fallbacks:
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.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.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/relatedResponse:
{
"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:
children (hero powers + buddies), maybe a companion (Duos partner), and skins (cosmetic portraits)parent (if they're a token), children (tokens they generate), and textMentions (cards referenced in their text)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/skinsEndpoint: 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.3Response:
{
"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/historyReturns every patch-notes entry that ever mentioned this card.
Endpoints:
GET /api/v1/patches — list all tracked patches with summariesGET /api/v1/patches/{version} — full patch-notes dataGET /api/v1/patches/latest — 302 redirect to the newest patchNo 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.2Use latest if you want the newest patch without hardcoding the number:
curl -L https://hsbg.cards/api/v1/patches/latestEndpoint: 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:
pool — current, legacy, upcoming, or allcardType — minion, hero, spell, trinket, etc.tier — 1–7 (tavern tier)tribe — beast, demon, dragon, etc.limit — page size (1–200)offset — page offsetfields — trim response to specific fields, e.g. id,name,text,atkThe response includes a total count and a hasMore flag so you know when to stop paginating.
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.
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.
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.
Every response — success or error — carries these headers:
X-RateLimit-Limit — total requests allowed in the current minuteX-RateLimit-Remaining — how many you have left in this windowX-RateLimit-Reset — Unix timestamp (seconds) when the window resetsRetry-After — only on 429 responses — how many seconds to wait before retryingExample of a healthy response:
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1744632180
Content-Type: application/jsonWhen 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:
?fields=id,name,image) to shrink payloads.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:
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.
| Alias | Canonical field |
|---|---|
text | text |
textgold, text_gold, goldentext | textGold |
atk, attack, power | attack |
atkgold, atk_gold, goldenatk | attackGold |
hp, health, life | health |
hpgold, hp_gold, goldenhp | healthGold |
mana, cost, manacost, gold | manaCost |
armor, armour | armor |
tier, level | tier |
flavor, flavortext, flavour | flavorText |
tribe, tribes, race, minionTypes | minionTypes |
kw, keywords | keywords |
children | childIds |
parent | parentId |
companion | companionId |
skins | skinIds |
mentions, textmentions | textMentionIds |
Canonical camelCase field names are always accepted verbatim.
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 versionsmessage — human-readable summaryhint — optional suggestion for how to fix it (e.g. typo correction)Common codes:
| Code | Status | Meaning |
|---|---|---|
card_not_found | 404 | No card matches the identifier |
field_not_found | 404 | Unknown field name |
patch_not_found | 404 | Unknown patch version |
skin_not_found | 404 | Unknown skin id |
image_not_available | 404 | Card has no image |
bad_request | 400 | Invalid or missing parameter |
batch_too_large | 400 | Batch > 100 identifiers |
invalid_image_variant | 400 | Unsupported size/format combo |
api_key_required | 401 | Missing or invalid key on suggest |
rate_limit_exceeded | 429 | Rate limit exceeded — see Retry-After |
internal_error | 500 | Unexpected 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.
/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-KeyYou 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.
/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:
Breaking changes (would ship as v2):
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