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

# Introduction

> A read-only HTTP API over a trading card catalog.

A read-only HTTP API over a catalog of trading cards, the expansions and series they belong
to, the sealed products that contain them, and the daily prices of all of it. Every price is an
integer number of cents — USD on the TCGplayer side, EUR on the Cardmarket side — never a
decimal. Everything is JSON, every read is a `GET`, and one endpoint takes a photo of a card and
tells you which card it is.

## Base URL

```
https://api.cromos.so
```

Every endpoint below `/v1` needs a key. See [Authentication](/authentication).

## Responses

Every read of the catalog — series, expansions, cards, products and their prices — answers with
the same two keys, and so does every failure. Exactly one of them is ever non-null, so checking
either one tells you about the other.

A detail endpoint puts the object in `data`:

```json theme={null}
{
  "data": { "id": "base1-4", "name": "Charizard", "rarity": "Rare" },
  "error": null
}
```

A list puts its rows and its cursor in `data` as well. `next_cursor` sits **inside** the
envelope rather than beside it, so `data` and `error` are the only two keys any response has:

```json theme={null}
{
  "data": {
    "items": [{ "id": "base1-1", "name": "Alakazam", "rarity": "Rare" }],
    "next_cursor": "eyJlIjoiYmFzZTEiLCJuIjozLCJpZCI6ImJhc2UxLTMifQ"
  },
  "error": null
}
```

`next_cursor` is `null` on the last page.

## Errors

Every failure has the same shape, and the `code` is the part to branch on:

```json theme={null}
{ "data": null, "error": { "code": "not_found", "message": "card not found" } }
```

`message` is always present and never empty — it is for a human reading a log, and its wording
is not part of the contract. Branch on `code`.

| Status | Code           | Meaning                                                   |
| ------ | -------------- | --------------------------------------------------------- |
| 400    | `bad_request`  | Parameters or body the API could not accept               |
| 401    | `unauthorized` | Key missing, malformed, unknown, revoked or expired       |
| 404    | `not_found`    | No such id                                                |
| 429    | `rate_limited` | Over the request budget — see [Rate limits](/rate-limits) |
| 500    | `internal`     | Our fault. Retry                                          |

`POST /v1/scan` adds codes of its own; they are listed on [Scan a card](/scan).

## Paging

List endpoints page by cursor, not by offset — rows do not shift under you between pages.

| Parameter | Meaning                                                                       |
| --------- | ----------------------------------------------------------------------------- |
| `limit`   | Rows per page. Defaults to 50, maximum 200                                    |
| `cursor`  | The `data.next_cursor` from the previous response. Omit it for the first page |

A `limit` above the maximum is clamped, not refused, and a value that is not a number falls
back to the default.

Walking a whole expansion means calling until `next_cursor` comes back `null` — here, three
cards at a time:

```bash theme={null}
curl "https://api.cromos.so/v1/cards?expansion_id=base1&limit=3" \
  -H "Authorization: Bearer $API_KEY"

curl "https://api.cromos.so/v1/cards?expansion_id=base1&limit=3&cursor=eyJlIjoiYmFzZTEiLCJuIjozLCJpZCI6ImJhc2UxLTMifQ" \
  -H "Authorization: Bearer $API_KEY"
```

Treat a cursor as opaque. It encodes sort position, and constructing one by hand will break.
Searching with `q` changes what a page contains, not how you walk it: the rows come back
best-match first, each carrying a `score`, and the same cursor keeps working.

## Searching

There is no search endpoint. Every list takes `q` and searches its own resource:
[cards](/cards/list) by name or collector number, [series](/series/list),
[expansions](/expansions/list) and [products](/products/list) by name. `q` composes with that
list's own filters, so `?q=charizard&expansion_id=base1` searches inside one expansion.

What comes back is a ranked set of candidates rather than one resolved id. See
[List cards](/cards/list) for how the ranking and its ties behave, and [Scan a card](/scan) to
identify a card from a photo instead of a name.

## Attaching related data

Some endpoints take `include` with a comma-separated list. Today only `prices` is understood,
on [List cards](/cards/list). Without it the `prices` key is **absent**, not
`null` and not `[]`, so a response stays byte-identical to what it was before the field
existed.

## Caching

`/v1/*` responses carry an `ETag`. Send it back as `If-None-Match` and an unchanged resource
answers `304 Not Modified` with no body — cheaper to receive and cheaper to parse. It is not
cheaper against your [rate limit](/rate-limits): the limiter counts the request before the
`304` is produced, so a conditional request that comes back unchanged costs exactly as much of
your budget as a full `200` would.

## Versioning

The version is in the path. `/v1` is current, and a breaking change would arrive as `/v2`
rather than by altering `/v1` under you. Adding a field to a response is not breaking, so
parse defensively: ignore keys you do not recognise.

## Service health

`GET /health` is unauthenticated and reports whether the data being served is current:

```bash theme={null}
curl https://api.cromos.so/health
```

`status` is `ok` or `degraded`. Degraded means a background task has not succeeded inside its
window, so some corner of the catalog is staler than it should be — the API keeps answering.
