> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elestrals.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Banlists & formats

> How card legality is encoded across organized-play formats.

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:

```json theme={"dark"}
{
  "id": "et-100",
  "name": "Pyrotter",
  "banlists": [
    {
      "id": "ble-1",
      "format_id": "fm-1001",
      "legality": "legal",
      "effective_date": "2024-09-01",
      "lift_date": null
    },
    {
      "id": "ble-2",
      "format_id": "fm-1002",
      "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

| Field            | Type             | Meaning                                                                                                                                                                         |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`             | `string`         | Stable identifier for this entry. Use it to reference a specific ruling.                                                                                                        |
| `format_id`      | `string \| null` | The 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. |
| `legality`       | `string`         | One of: `legal`, `semi_limited`, `limited`, `banned`, `illegal`. See [Enums → legality](/enums#legality).                                                                       |
| `effective_date` | `string \| null` | ISO date the entry started taking effect. `null` means "since the format's inception".                                                                                          |
| `lift_date`      | `string \| null` | ISO 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`.

```javascript theme={"dark"}
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

`format_id` is an opaque reference following the same `fm-<suffix>` shape as other resource IDs in the catalog. We don't ship a `/v1/formats` resource today, so there's no programmatic way to map a `format_id` to a human-readable name from the API surface alone.

For now, treat `format_id` as a stable string you can group on, filter against, and persist. Don't try to parse it. If your application needs to display a friendly format label, hard-code a small lookup table in your client based on the IDs you've observed in real card data.

If a `/v1/formats` endpoint lands later, this section will be updated and the mapping will become discoverable at runtime. New `format_id` values may appear at any time as Elestrals adds organized-play tracks; existing values are stable.

## 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](/caching) filter on `/v1/cards` if you need to keep a cache fresh.
