Collections
List Collections
/api/v1/collectionsReturns a paginated list of the user's card collections.
| Parameter | Required | Description |
|---|---|---|
limit | No | Number of results (1-100, default 20) |
cursor | No | Cursor from previous response for pagination |
{
"data": [
{
"id": "550e8400-...",
"name": "My Collection",
"card_count": 1247,
"created_at": "2026-02-10T08:00:00Z"
}
],
"meta": {
"request_id": "req_xyz",
"timestamp": "2026-03-31T14:22:00Z",
"has_more": false,
"next_cursor": null
}
}Get Collection
/api/v1/collections/{id}Returns a collection with a page of its cards. Follow next_cursor until has_more is false to read the whole collection. card_count is the total for the collection, not the page. Cards are ordered by oracle_id, which is stable across pages; sort client-side if you need them alphabetical.
| Parameter | Required | Description |
|---|---|---|
limit | No | Cards per page (1-500, default 100) |
cursor | No | next_cursor from the previous response. A malformed cursor returns 400 invalid_cursor rather than silently restarting at the first page. |
color | No | Comma-separated colours: W, U, B, R, G, or C for colourless. Values are OR-ed, so color=W,U returns cards playable with white or blue available. C matches cards with no colour and cannot be expressed by listing the other five. |
color_match | No | identity (default) matches colour identity, which is what Commander legality uses. colors matches the card's own casting colours. They differ: a land with a coloured ability has no casting colour but does have a colour identity. Sending color_match without color returns 400. |
type | No | Comma-separated card types: artifact, battle, creature, enchantment, instant, kindred, land, planeswalker, sorcery. Values are OR-ed. Matches the FRONT face and the types only, so a Saga that transforms into a creature is an enchantment, not a creature, and an Aura that enchants a creature is not one either. Subtypes are not accepted; type=wizard returns 400. |
An unrecognised filter value returns 400 invalid_filter naming the field and listing what is supported, rather than being ignored. A silently dropped value would return a page that quietly answered a different question. card_count is always the whole collection and does not shrink when a filter is applied, so meta.filters echoes back what was actually filtered.
{
"data": {
"id": "550e8400-...",
"name": "My Collection",
"card_count": 1247,
"created_at": "2026-02-10T08:00:00Z",
"cards": [
{
"oracle_id": "a1b2c3d4-...",
"name": "Sol Ring",
"mana_cost": "{1}",
"type_line": "Artifact",
"colors": [],
"color_identity": [],
"quantity": 3
}
]
},
"meta": {
"request_id": "req_xyz",
"timestamp": "2026-03-31T14:22:00Z",
"has_more": true,
"next_cursor": "6c1c4c1a-9c39-4b4e-9f8f-2f6f1a0d5b21",
"filters": { "color": ["G"], "color_match": "identity", "type": ["creature"] }
}
}Create Collection
/api/v1/collectionsCreates a new collection, optionally with cards. Respects your plan's collection limit.
| Header | Required | Description |
|---|---|---|
Idempotency-Key | No | Any string of 1-255 printable ASCII characters, unique per distinct request (a UUID works). A retry with the same key and the same body returns the stored response with Idempotent-Replayed: true instead of importing again. The same key with a different body returns 422 idempotency_key_reused. Keys expire after 24 hours. |
{
"name": "Gestic Import - March 2026",
"cards": [
{ "oracle_id": "0004ebd0-dfd6-4276-b4a6-de0003e94237", "quantity": 2 },
{ "name": "Sol Ring", "quantity": 3 },
{ "name": "Fire // Ice", "quantity": 1 }
]
}{
"collection_id": "550e8400-...",
"collection_name": "Gestic Import - March 2026",
"added": 2,
"updated": 1,
"errors": [],
"capacity": { "used": 1250, "limit": 20000, "remaining": 18750 }
}If the collection name is omitted, a default name is generated. If the card list cannot be imported at all (for example, the collection limit is reached), no collection is created.
Identify each card by oracle_id (preferred: exact, and cards that share a name are never merged) or by name. If a row has both, the card is matched by oracle_id; the row's other checks, such as the name length limit, still apply. An empty oracle_id ("" or null) is treated as not sent, so the row is matched by name. Names for split, adventure and double-faced cards are accepted in full ("Fire // Ice") or as either face ("Fire"). If a card appears more than once in a request, its quantities are added together.
Per-row error reasons, each with the row's index: not_found (name not recognized, or oracle_id unknown), invalid_oracle_id (oracle_id is not a UUID), capacity_limit (plan limit reached), invalid_quantity (quantity must be 1-1000), missing_identifier (no name or oracle_id provided), name_too_long (over 200 characters). Request-level errors: 400 batch_too_large (over 10,000 cards), 400 invalid_idempotency_key, 409 import_in_progress (another import for the same user is still running; retry after the Retry-After header), 422 idempotency_key_reused.
Import Cards
/api/v1/collections/{id}/cardsAdds cards to an existing collection. If a card already exists, its quantity is incremented (upsert). Respects your plan's card capacity limit. Cards that exceed the limit are skipped.
| Header | Required | Description |
|---|---|---|
Idempotency-Key | No | Any string of 1-255 printable ASCII characters, unique per distinct request (a UUID works). A retry with the same key and the same body returns the stored response with Idempotent-Replayed: true instead of importing again. The same key with a different body returns 422 idempotency_key_reused. Keys expire after 24 hours. |
{
"cards": [
{ "name": "Lightning Bolt", "quantity": 4 },
{ "name": "Nonexistent Card", "quantity": 1 }
]
}{
"collection_id": "550e8400-...",
"added": 1,
"updated": 0,
"errors": [
{ "index": 1, "name": "Nonexistent Card", "reason": "not_found" }
],
"capacity": { "used": 1251, "limit": 20000, "remaining": 18749 }
}Identify each card by oracle_id (preferred: exact, and cards that share a name are never merged) or by name. If a row has both, the card is matched by oracle_id; the row's other checks, such as the name length limit, still apply. An empty oracle_id ("" or null) is treated as not sent, so the row is matched by name. Names for split, adventure and double-faced cards are accepted in full ("Fire // Ice") or as either face ("Fire"). If a card appears more than once in a request, its quantities are added together.
Per-row error reasons, each with the row's index: not_found (name not recognized, or oracle_id unknown), invalid_oracle_id (oracle_id is not a UUID), capacity_limit (plan limit reached), invalid_quantity (quantity must be 1-1000), missing_identifier (no name or oracle_id provided), name_too_long (over 200 characters). Request-level errors: 400 batch_too_large (over 10,000 cards), 400 invalid_idempotency_key, 409 import_in_progress (another import for the same user is still running; retry after the Retry-After header), 422 idempotency_key_reused.
Importing Large Collections
Send up to 10,000 cards per request. A 10,000-card request typically completes in a few seconds, so a 50,000-card collection is a handful of calls: create the collection with the first batch, then send the rest to /collections/{id}/cards.
- Send batches one after another. Imports for the same user run one at a time on our side so the capacity check stays exact. Parallel requests for one user wait for each other and do not finish sooner, and a request that waits more than a few seconds returns
409 import_in_progresswith nothing written. - Use an
Idempotency-Keyper batch. Quantities are additive, so retrying a request that actually succeeded would count those cards twice. With a key, the retry returns the original response instead. - Map errors with
index. Each error'sindexis the position in that request'scardsarray, starting at 0 for every request. It is not a position in your full list. - Check
capacity. The plan limit counts distinct cards across all of the user's collections. Rows past it come back ascapacity_limit.