Decks
List Decks
/api/v1/decksReturns a paginated list of the user's decks.
| Parameter | Required | Description |
|---|---|---|
limit | No | Number of results (1-100, default 20) |
cursor | No | Cursor from previous response for pagination |
{
"data": [
{
"id": "660f9500-...",
"name": "Atraxa Superfriends",
"format": "commander",
"commander_name": "Atraxa, Praetors' Voice",
"engine_version": "4.6",
"target_bracket": 3,
"bracket": {
"target": 3,
"actual": 2,
"status": "ok",
"checked_at": "2026-03-20T12:04:11Z",
"basis": "compliance",
"measures": "Game changer count and illegal or banned combo count. Not a power rating.",
"max_detectable": 4
},
"origin": "built",
"import_source": null,
"build": {
"tier": "Mythic",
"pool_size": 1843,
"pool_size_meaning": "Distinct cards in the source collection that are legal in the commander's colour identity, not the size of the collection.",
"completed_at": "2026-03-20T12:03:58Z"
},
"created_at": "2026-03-20T12:00:00Z"
}
],
"meta": {
"has_more": true,
"next_cursor": "660f9500-..."
}
}Caching Spellweave Analysis
Responses that carry computed analysis, such as /decks/{id}/stats?include= and the Deck Lab, tell you how long you may keep the result. The period appears twice: as a Cache-Control header for your HTTP client, and as meta.cache for you.
Cache-Control: private, max-age=86400
"meta": {
"cache": {
"max_age_seconds": 86400,
"expires_at": "2026-09-07T18:28:39Z"
}
}The Developer Terms bind you to this period rather than to a number written into the terms, so it can be tuned per endpoint without a new version of the agreement. privateis deliberate: this is one user's analysis behind their token and must not be stored in a shared or CDN cache. Store expires_at alongside anything you persist, and re-fetch after it passes. Raw user data such as deck and collection lists carries no such limit.
Reading the bracket
bracket.target is what the user asked for (1 to 5, or null for no preference). bracket.actual is what our deterministic checker found, and it is a compliance readout, not a measure of how strong the deck is. Its only inputs are the game changer count and the illegal or banned combo count, so a well-tuned deck with neither will read Bracket 2. Do not present it to your users as a power rating.
It ranges 1 to 4 and never returns 5, because cEDH is a metagame designation that a card list alone cannot certify. A deck targeting 5 whose actual is 4 is at the top of what the checker can see, and is not behind its target.
The check runs after the build, so actual is null until it completes. Always read status before actual: pending means not computed yet, ok means the value is good, failed means the check errored, and unsupported means the format has no checker. A null actual never means bracket zero.
Built or imported
origin is built for decks our engine built and imported for decks brought in from elsewhere, where import_source names the platform. build is present only for built decks. Inside it, tier is the optimization tier the build ran at (Uncommon, Rare or Mythic), and pool_size is how many distinct cards the optimizer had to choose from: the cards in the source collection that are legal in the commander's colour identity. It is not the size of the collection, and a 20,000-card collection can present a pool of under 2,000 for a mono-colour commander. Either field can be null for older decks whose build record predates them; null means unknown, never zero.
Get Deck
/api/v1/decks/{id}Returns a deck with all its cards.
{
"data": {
"id": "660f9500-...",
"name": "Atraxa Superfriends",
"format": "commander",
"commander_name": "Atraxa, Praetors' Voice",
"commander_oracle_id": "a1b2c3d4-...",
"engine_version": "4.6",
"target_bracket": 3,
"bracket": {
"target": 3,
"actual": 2,
"status": "ok",
"checked_at": "2026-03-20T12:04:11Z",
"basis": "compliance",
"measures": "Game changer count and illegal or banned combo count. Not a power rating.",
"max_detectable": 4
},
"origin": "built",
"import_source": null,
"build": {
"tier": "Mythic",
"pool_size": 1843,
"pool_size_meaning": "Distinct cards in the source collection that are legal in the commander's colour identity, not the size of the collection.",
"completed_at": "2026-03-20T12:03:58Z"
},
"created_at": "2026-03-20T12:00:00Z",
"cards": [
{
"oracle_id": "a1b2c3d4-...",
"name": "Atraxa, Praetors' Voice",
"mana_cost": "{G}{W}{U}{B}",
"mana_value": 4.0,
"type_line": "Legendary Creature — Phyrexian Angel Horror",
"quantity": 1,
"is_commander": true,
"category": "commander"
},
{
"oracle_id": "e5f6g7h8-...",
"name": "Deepglow Skate",
"mana_cost": "{4}{U}",
"mana_value": 5.0,
"type_line": "Creature — Fish",
"quantity": 1,
"is_commander": false,
"category": "superfriends"
}
]
}
}Deck Statistics
/api/v1/decks/{id}/statsCard counts by type, land count and average mana value. Pass include to add the deeper sections Spellweave computes for the deck page. Everything here is deterministic and costs the same whether you ask for one section or all of them.
The response WITHOUT include is unchanged, so existing integrations are unaffected. Adding include adds keys inside data and an attribution block; it never removes or renames anything.
| Parameter | Required | Description |
|---|---|---|
include | No | Comma-separated: fundamentals, curve, bracket, or all. An unrecognised value returns 400 invalid_include rather than being ignored. |
| include | Adds |
|---|---|
fundamentals | archetype, secondary_archetype, and fundamentals: per-role actual, target and gap. |
curve | mana_curve: the histogram. Land count and average mana value are already in the base response and are not repeated. |
bracket | bracket: the Commander bracket with its name, game changers, and detected combos. |
all | All of the above. |
This is NOT the AI Analysis. That is a separate paid feature and is not available through the API yet.
curl "https://public-api.spellweave.app/api/v1/decks/{id}/stats?include=fundamentals,bracket" -H "Authorization: Bearer $SPELLWEAVE_TOKEN"{
"data": {
"deck_id": "660f9500-...",
"total_cards": 100,
"total_quantity": 100,
"creature_count": 28,
"spell_count": 18,
"artifact_count": 12,
"enchantment_count": 8,
"land_count": 36,
"avg_mana_value": 3.14,
"archetype": "counters_plus1",
"secondary_archetype": "enchantress",
"fundamentals": {
"ramp": { "actual": 12, "target": 10, "gap": 0 },
"draw": { "actual": 8, "target": 10, "gap": 2 },
"removal": { "actual": 9, "target": 10, "gap": 1 }
},
"bracket": {
"bracket": 2,
"bracket_name": "Core",
"game_changers": [],
"combos": []
}
},
"attribution": {
"text": "Deck analysis by Spellweave",
"url": "https://spellweave.app",
"required": true,
"notice": "If you display this data in your product, you must disclose that it comes from spellweave.app. See the Spellweave Developer Terms of Service."
},
"meta": { "request_id": "req_xyz", "timestamp": "2026-09-05T19:00:00Z" }
}Import a Deck
/api/v1/decksImports a deck into the authenticated user's account. Send either a decklist or a Moxfield or Archidekt deck URL in deck. Both go through the same parser and the same validation the Spellweave app uses, so a deck that imports here imports there.
| Parameter | Required | Description |
|---|---|---|
deck | Yes | A decklist, or a Moxfield/Archidekt deck URL. Max 100,000 characters. |
format | No | Defaults to commander. Only commander is accepted today; standard is recognised and returns format_not_yet_supported. |
name | No | Deck name. Derived from the source when omitted. |
source | No | Your own provenance label, stored on the deck. Defaults to api. |
{
"deck": "https://moxfield.com/decks/xxxxxxxxxxxxxxx",
"format": "commander",
"name": "Atraxa Superfriends"
}{
"data": {
"id": "660f9500-...",
"name": "Atraxa Superfriends",
"format": "commander",
"commander": { "name": "Atraxa, Praetors' Voice", "oracle_id": "..." },
"partner": null,
"requested_cards": 100,
"resolved_cards": 100,
"unresolved_cards": [],
"warnings": [],
"detected_source": "moxfield"
},
"attribution": {
"text": "Deck analysis by Spellweave",
"url": "https://spellweave.app",
"required": true,
"notice": "If you display this data in your product, you must disclose that it comes from spellweave.app. See the Spellweave Developer Terms of Service."
}
}resolved_cards can be lower than requested_cards. Cards that did not resolve are listed in unresolved_cards and are NOT in the deck, so show them to your user rather than assuming the import was complete.
Deck requirements
Commander decks must be exactly 100 cards including the commander. A deck with two partner commanders is 98 + 2, which is also exactly 100. The commander must be legal as a commander on its FRONT face: a card whose Legendary Creature type sits on the back is rejected.
Errors
Every failure carries a machine-readable code. Size failures also carry a details object with the numbers needed to fix the deck, so you can tell your user exactly what to change rather than that something went wrong.
{
"error": {
"code": "INVALID_DECK_SIZE",
"message": "This Commander deck has 98 cards. Commander decks must have exactly 100 cards including the commander. Add 2 cards and try again.",
"details": {
"totalCards": 98,
"expected": 100,
"delta": 2,
"direction": "add",
"commanderZoneCount": 1,
"format": "commander"
},
"documentation": "https://dev.spellweave.app/docs/errors/"
}
}| Code | Meaning |
|---|---|
INVALID_DECK_SIZE | Not exactly 100 cards. details tells you how many to add or remove. |
INVALID_COMMANDER | The detected commander is not legal as a commander (front face is checked). |
NO_COMMANDER | No commander could be detected. Mark it with a Commander section or category. |
NO_CARDS_FOUND | Nothing parseable in deck. |
COMMANDER_NOT_FOUND | The commander name did not match a card. |
UNSUPPORTED_URL | Only Moxfield and Archidekt deck URLs are supported. |
URL_FETCH_FAILED | The deck URL could not be fetched. It may be private. |
URL_FETCH_EMPTY | The URL returned no deck data. |
DECK_TEXT_TOO_LONG | Over 100,000 characters. |
deck_required | deck was missing or blank. |
invalid_format | Unrecognised format. |
format_not_yet_supported | A known format that this endpoint does not accept yet. |
DECK_SLOT_LIMIT | The user is at their deck slot limit. Surface this to them; retrying will not help. |
Deck Lab
/api/v1/decks/{id}/labThe Deck Lab goldfish simulation: speed timeline, representative game, and distributions over a thousand simulated games.
This is a paid feature and is not available for every deck. It unlocks when the deck owner runs an AI Analysis, or completes a Deck Tune, on spellweave.app. Until then this endpoint returns 403 lab_not_unlocked with an unlock_url you can send the user to. A deck that is unlocked but whose Lab has never been generated returns 404 lab_not_generated.
Analysis and Tune
/api/v1/decks/{id}/analysis/api/v1/decks/{id}/tuneBoth routes exist and return 501 not_implementedtoday, so you can tell "not built yet" from a bad request while you build against the API. AI Analysis and Deck Tune are paid features on spellweave.app; exposing them here is planned but not scheduled.
Attribution
Every response carrying computed deck data includes an attribution block. If you display any of this data in your product, you must disclose to your users that it comes from spellweave.app. This is a condition of the Spellweave Developer Terms of Service, which you accepted when you registered your app.
The block travels with the data so the requirement is visible where you consume it, not only in a document. A link reading "Deck analysis by Spellweave" pointing at spellweave.app satisfies it.