Partners
Commander Bracket classifies Magic: The Gathering Commander decks into a 1-5 power bracket from the actual cards. The same engine that powers our app and iOS app is available to partners through a simple API: send a decklist, get back a bracket and the reasoning.
Four integrations
POST a decklist (any common format - Moxfield, Archidekt, MTG Arena, plain text) and render the deck's bracket (1-5), a one-line reason, and a plain-language narrative. Results are deterministic for a given decklist, so they cache cleanly. This is the 15-minute integration for deck sites and tools.
The response carries more than the badge, and it is all yours to render: estimated_win_turn (the typical turn this deck wins on), deep_link (an attributed link to the full analysis), why_this_bracket_factors (the reasoning as a labeled list), bracket_fit (whether the rating comes from speed or from what is in the deck), classification_confidence (how close the deck sits to a bracket boundary, for a "Borderline 3/4" pill), card_count (how many cards we scored - assert 100 to catch unmarked sideboards), tutor_tier_summary, and floor_summary.
floor_summary is one headline sentence saying what set the bracket, plus the reasons behind it. Each reason carries the full list of cards that triggered it, and the Game Changers reason carries the numeric limit (3 below Bracket 4, 0 below Bracket 3), so you can show exactly which cards put the deck where it is.
POST /brackets/deckTwo more blocks on the same call. Add "include_experience": true to the body and you get back archenemy, how unpleasant the deck is to sit across from, and deck_health, how well built it is against decks of the same bracket and engine. Leave the flag out and the response is unchanged, so nothing you already ship has to move.
Archenemy is a pod-experience axis, not a second power scale. It is close to uncorrelated with bracket by design, so a friendly deck can still be a miserable one to play against. It comes back as a 0-100 score plus a named level and the full ladder in levels - render the name, and read the ladder off the wire rather than hardcoding it. Deck health is a 0-100 composite over three pillars, cohort relative, so a good deck lands in the 70s. Both blocks carry card-level drill-in, so you can show which cards drove the number instead of a bare score.
The two withhold differently on purpose. When we cannot score a deck, deck_health comes back present and null, and archenemy is absent entirely. Hide the panel in both cases. Never substitute a zero - a zero archenemy score reads as "this deck is harmless", which is the one thing we should not say when we do not know.
POST /brackets/deck with include_experienceFor scanner and collection apps: push a user's collection, then suggest the commanders it actually supports, build a full deck at a target bracket from cards they own, upgrade decks they already have, and show the short missing-cards list with ready-made purchase links. Working copies are isolated to your key, auto-purged after 30 days, and deletable on request.
/collections/*, /commander/build-and-analyze, /decks/adjust-bracketFor Mystery Commander style modes: roll a commander out of GET /commander/search (filter by color identity, engine, creature type or EDHREC popularity), then send it to POST /commander/build-and-analyze with a target bracket and get back a full built deck, its bracket analysis, and a deep_link to the full analysis on our site. The build takes commander, partner and power_level only. Search is in the standard bracket bundle; the build endpoint is enabled on request.
GET /commander/search, POST /commander/build-and-analyzehttps://mtg-assistant.up.railway.appX-API-Key header (server-side only)Keys are scoped to exactly the endpoints you need. Every response carries rate-limit headers; treat them as approximate, since the counters are kept per server process rather than shared. Every key can also call GET /whoami to confirm it works before you debug anything else: it returns your partner id, tier, remaining daily and AI-call budget, and a request id. Responses are in English; decklists can use Spanish, Portuguese and flavor card names. Card data should come from Scryfall directly - we do not proxy it. We send the full integration guide and your key when you onboard.
Deck import and account connect
Every shared deck is fetchable as plain text at /shared-decks/{token}/decklist, no API key needed, so your importer can accept Commander Bracket share links directly. Send that one without your key: it is a public route rather than part of the partner surface, so a keyed request is rejected with a 403 even though the key is valid. Partners can also offer "Connect your Commander Bracket account": the user pastes their public creator-page link and you pull their published decks, decklists and brackets in one call via GET /partner/profiles/{profile_token}/decks, which is granted per key on request rather than included in a bundle. We send the full details when you onboard.
Feedback buttons
Put two buttons under any bracket you render. Votes go to POST /brackets/feedback with the same X-API-Key, and land in the same training corpus our own app's feedback does. What your users push back on moves the brackets you get back from us. 100% of the labeled decks behind our published accuracy numbers arrived through a partner's feedback buttons.
- Looks right sends
agree: truewithuser_bracketset to the bracket you showed. - It's really a... opens a 1-5 picker and sends their pick as
user_bracket, withagreefalse or omitted. A free-textnotesbox here is the single most useful thing you can collect - it is what tells us which signal we read wrong.
Send bracket_given on both. It is the bracket your app had on screen at vote time, and it is what lets us score the disagreement in the direction the user actually saw. Leave it out and we recompute server-side, which can land somewhere other than what you displayed.
// Fires when the user taps one of your two buttons.
// Server-side only - never ship the key to a browser or app binary.
await fetch('https://mtg-assistant.up.railway.app/brackets/feedback', {
method: 'POST',
headers: {
'X-API-Key': process.env.CB_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
decklist: cardNames, // the same list you sent to /brackets/deck
commander: 'Kinnan, Bonder Prodigy',
bracket_given: shownBracket, // what your UI displayed (send this)
user_bracket: agreed ? shownBracket : theirPick,
agree: agreed, // true only when they accepted it
notes: theirComment, // optional, free text
}),
})
// -> 200 { "status": "received", "source": "partner:your-slug" }deckliststring or string[], required - newline-separated names or an array. The same deck you sent to/brackets/deck.user_bracketint 1-5, required - the bracket the user says is correct.bracket_givenint 1-5, optional - the bracket you displayed. Must equaluser_bracketwhenagreeis true.agreebool, optional - true when they accepted the shown bracket.commanderstring, optional - improves attribution and dedup.notesstring, optional - what felt wrong, in their words.
- 200
{ status: "received", source: "partner:your-slug" }- the slug is derived from your key, so a vote cannot be attributed to anyone else. Approvals are tagged:approve. The body carries some internal record fields alongside these; branch onstatusandsourceand ignore keys you don't recognise. - 400 - bracket outside 1-5, empty decklist, over 500 cards, or
agree: truewith abracket_giventhat disagrees withuser_bracket. - 401 / 403 - key missing or invalid, or valid but not enabled for this route. Ask us and we will add it.
- 429 - over your key's per-minute or daily limit, shared with
/brackets/deck.
Fire it on a real user reaction, not on every analysis. Votes are stored as labeled training examples and used only to improve bracket classification. Partner corrections train at a conservative weight to start; we raise it as a stream proves out. Don't put end-user identifiers in notes.
Attribution
Any surface that shows Commander Bracket data must display "Powered by Commander Bracket" linking back to commanderbracket.app with your ?ref=<your-slug> on the URL. That parameter is how partner traffic is attributed, and it is what we look at when we judge whether the free tier is working. The attribution kit has the wordmark, a copy-paste badge, and usage guidelines.
Pilot terms
Blurb-level and pilot-stage (not a formal agreement); these describe the intended terms and are subject to a written agreement:
- The API and data are provided AS-IS, without warranty.
- Don't exceed documented rate limits or work around them.
- No bulk data extraction for competitive purposes; no use to clone the bracket-classification product.
- Don't use the API to bypass third-party rate limits (Moxfield, Scryfall).
- Don't represent API output as official from Wizards of the Coast.
- Attribution is mandatory.
- Don't send end-user PII (emails, names, payment info) in request bodies; use opaque identifiers.
- Free during the pilot. We will give plenty of notice before any change, and keep a free tier for pilot partners.
Third-party data: portions are property of Wizards of the Coast - Commander Bracket is unofficial Fan Content permitted under the WotC Fan Content Policy. Scryfall, EDHREC, and Commander Spellbook attribution is required where their data is surfaced; no bulk redistribution.
Get a key
Want to integrate? Email me with your app, the surface you'd show our data on, and your preferred attribution slug, and I'll set you up.
keith@commanderbracket.app