Skip to main content
A Card’s legality is not a single field — it’s an array of BanlistEntry rows, one per format the Card has been ruled on. This page explains why, and how to read them.

The shape

Every Card carries a banlists array:
{
  "id": "et-100",
  "name": "Pyrotter",
  "banlists": [
    {
      "id": "ble-1",
      "format_id": "fmt-standard",
      "legality": "legal",
      "effective_date": "2024-09-01",
      "lift_date": null
    },
    {
      "id": "ble-2",
      "format_id": "fmt-classic",
      "legality": "limited",
      "effective_date": "2024-12-15",
      "lift_date": null
    }
  ]
}
Each entry binds three things together: a Card, a format, and a legality status, with optional dates that bound the ruling.

Fields

FieldTypeMeaning
idstringStable identifier for this entry. Use it to reference a specific ruling.
format_idstring | nullThe format this entry applies to. null means the entry applies globally — rare; typically only used for cards that are illegal everywhere because they were never released.
legalitystringOne of: legal, semi_limited, limited, banned, illegal. See Enums → legality.
effective_datestring | nullISO date the entry started taking effect. null means “since the format’s inception”.
lift_datestring | nullISO date the entry stops taking effect. null means “still in effect today”.

How to read it

To know a Card’s legality right now, in a specific format:
  1. Filter banlists to entries where format_id matches the format you care about.
  2. Filter to entries where effective_date is in the past (or null) and lift_date is in the future (or null).
  3. The entry that survives is the active legality. There should be exactly one; if there are more, take the most recent effective_date.
function legalityFor(card, formatId, now = new Date()) {
  const today = now.toISOString().slice(0, 10);
  const matches = card.banlists.filter(
    (b) =>
      b.format_id === formatId &&
      (b.effective_date === null || b.effective_date <= today) &&
      (b.lift_date === null || b.lift_date > today),
  );
  if (matches.length === 0) return 'legal'; // no entry = legal by default
  matches.sort((a, b) => (b.effective_date ?? '').localeCompare(a.effective_date ?? ''));
  return matches[0].legality;
}
The “no entry = legal by default” case is intentional — we only ship BanlistEntry rows for restrictions, not for the affirmative “this card is legal.” A clean banlist array means the Card is legal in every active format.

Historical lookups

Because entries carry effective_date and lift_date, you can ask “was this Card legal in Standard on 2024-10-01?” without any external state. Pass a different now to the snippet above, and the same logic does point-in-time lookups. This is the reason banlist entries are not removed when superseded — they’re closed off with lift_date and a new entry takes effect from the next day.

Formats themselves

The API exposes formats as opaque references in format_id. We don’t ship a /v1/formats resource today, but the canonical IDs in use are:
format_idDescription
fmt-standardCurrent Standard format. Most recent rotation.
fmt-extendedExtended — broader card pool than Standard.
fmt-classicAll-time format. Every released Card eligible.
fmt-organized_playOP-specific rulings (regionals, internationals).
These IDs are stable but the list may grow as new formats launch. If you see a format_id you don’t recognize, treat the entry as informational and don’t gate anything on it.

Printing-level banlists

BanlistEntry also appears on the Printing DTO. This is unusual — most TCG APIs only ban at the Card level — but Elestrals occasionally rules on specific printings (e.g., a tournament-prize printing of an otherwise legal Card might be banned in standard play because the print run distorts the secondary market). When a printing has its own banlists array, treat it as additive: a printing is restricted if either its parent Card or the printing itself has an active restriction.

Common mistakes

  • Treating banlists: [] as banned: true. It’s the opposite. Empty array means no restriction.
  • Comparing effective_date as a Date object. They’re plain ISO date strings (YYYY-MM-DD). String-comparing them is correct and faster.
  • Caching legality per Card without a TTL. Banlists update on a regular cadence (typically quarterly). Use the updated_since filter on /v1/cards if you need to keep a cache fresh.