Broken Oath · for tool makers

Public read API

I built this so you can make ladder sites, build pages and meta trackers for Broken Oath. It is a set of plain JSON files on a CDN. No key, no login, no sign up, CORS open. You just download files, and nothing you do can touch the game or its servers.

FormatJSON over HTTPS GET
AuthNone. No key, no login
CORSOpen to every site
LadderEvery 15 minutes
Builds + metaEvery 60 minutes

Base URL

https://api.playbrokenoath.com/v1/

Start at version.json. It lists the leagues, the refresh times, when the ladder and the builds were last written, and the game data files of the current patch.

Endpoints

Every file (except the game data files) starts with the same four fields:

A field with no value is left out of the file, so treat a missing field as null. Ids of things the community has not discovered yet (secret runewords, uniques, recipes, nodes, supports) show as "hidden".

The examples below are trimmed outputs of the real publisher code from a test run, so the league ids, skills and numbers in them are test data.

version.json

every 15 minstart here

Refresh times, the public league ids, ladder_updated_at, builds_updated_at (the last hourly run), builds_published, the game data patch with its file list, and the endpoint list.

{
  "schema": 1,
  "generated_at": "2026-11-10T21:46:48.258Z",
  "game_build": 64,
  "patch": "0.68.0",
  "api": "v1",
  "base": "https://api.playbrokenoath.com/v1/",
  "cache_seconds": 300,
  "ladder_updated_at": "2026-11-10T21:46:48.258Z",
  "builds_updated_at": "2026-11-10T21:46:48.258Z",
  "refresh": { "ladder_minutes": 15, "leagues_minutes": 15, "builds_minutes": 60,
               "meta_minutes": 60, "game_data": "every patch" },
  "leagues": ["eat-sc-trade", "eat-sc-ssf", "eat-hc-trade", "eat-hc-ssf", "pt-sc-trade"],
  "builds_published": true,
  "game_data": { "game_build": 68, "patch": "0.68.0", "files": ["passive_tree.json"] },
  "endpoints": ["version.json", "leagues.json", "ladder/<league>.json",
                "builds/<league>/<class>.json", "builds/<league>/<class>/<hero>.json",
                "meta/<league>.json", "game-data/index.json", "game-data/<file>.json"]
}

If builds_published is false, no build files are written that run and every ladder row has has_build: false.

leagues.json

every 15 min

Every public league: official seasons only, still running or ended less than 14 days ago. Private custom leagues are never in it.

{
  "schema": 1, "generated_at": "2026-11-10T21:46:48.258Z", "game_build": 64, "patch": "0.68.0",
  "leagues": [
    { "id": "eat-sc-trade", "name": "Early Access Trials", "trade": true, "hardcore": false,
      "season": "early-access-trials", "season_name": "Early Access Trials",
      "starts_at": "2026-10-18T21:00:00-04:00", "ends_at": "2026-12-13T13:00:00-05:00" }
  ]
}

ladder/<league>.json

every 15 mintop 200 per class

The top 200 heroes of each class in one league, in overall ladder order. Ranked by best Rupture Level, then level, then experience, then fastest clear, then who got there first. Each row:

{
  "schema": 1, "generated_at": "2026-11-10T21:46:48.258Z", "game_build": 64, "patch": "0.68.0",
  "league": "zz1-hc-ssf", "top_per_class": 200, "builds_per_class": 50, "count": 3,
  "heroes": [
    { "rank": 1, "class_rank": 1, "hero": "a98f03f5f3aad95f", "class": "Mage", "ascendancy": "Frost",
      "level": 84, "rupture_level": 76, "best_at": "2026-11-10T16:46:43.624821-05:00", "dead": false,
      "main_skill": "split_shot", "main_supports": ["chain"], "guardian_dps_median": 5002,
      "updated_at": "2026-11-10T16:46:43.73075-05:00",
      "has_build": true, "build": "builds/zz1-hc-ssf/Mage/a98f03f5f3aad95f.json" }
  ]
}

builds/<league>/<class>.json

every 60 mintop 50 per class

The class build list: every hero of that class in that league that has a published build, in ladder order, with a short summary and the path of each build file. Class ids are the ones in the ladder's class field: Warrior, Mage, Necromancer, Paladin, Ranger, Samurai. A class with no heroes in a league has no list file.

{
  "schema": 1, "generated_at": "2026-11-10T21:46:48.258Z", "game_build": 64, "patch": "0.68.0",
  "league": "zz1-sc-trade", "class": "Mage", "count": 7,
  "heroes": [
    { "hero": "204bc62d559d5105", "class_rank": 1, "rank": 1, "ascendancy": "Ember", "level": 69,
      "rupture_level": 500, "dead": false, "main_skill": "split_shot", "main_supports": ["chain"],
      "guardian_dps_median": 5002, "updated_at": "2026-11-10T16:46:38.280707-05:00",
      "build": "builds/zz1-sc-trade/Mage/204bc62d559d5105.json" }
  ]
}

builds/<league>/<class>/<hero>.json

checked every 60 minrewritten only when it changed

One full build. Every worn item (slot, base, rarity, unique, item level, quality, sockets, runes, forged, affixes with id, value, value2, tier, crafted, oath), passive nodes and tree version, bonus passive points, the Dragonling (aspect, nodes, tree version), skills with their supports, gem levels, hotbar, main skill, damage per skill, and the game's own measured Guardian numbers: kills, losses, median dps and median kill time over the last day of Guardian fights.

The file disappears when the hero leaves the top 50 of its class, is hidden, deleted, or its player opts out. An item whose runes spell a secret Oathword shows no runes and "runes_hidden": true.

{
  "schema": 1, "generated_at": "2026-11-10T21:46:46.093Z", "game_build": 64, "patch": "0.68.0",
  "hero": "a98f03f5f3aad95f", "league": "zz1-hc-ssf", "class": "Mage", "ascendancy": "Frost",
  "level": 84, "rupture_level": 76, "class_rank": 1, "dead": false, "hardcore": true,
  "updated_at": "2026-11-10T16:46:43.726732-05:00",
  "measured": { "at": "2026-11-10T16:46:46.19383-05:00", "window_days": 1,
                "guardian_kills": 3, "guardian_losses": 0,
                "guardian_dps_median": 5002, "guardian_kill_seconds_median": 30 },
  "build": {
    "items": [
      { "slot": "Ring1", "base": "copper_band", "rarity": "Rare", "item_level": 80, "quality": 5,
        "sockets": 0, "runes": [], "forged": 0,
        "affixes": [{ "id": "Life", "value": 84, "value2": 0, "tier": 1, "crafted": false, "oath": false }] },
      { "slot": "Body", "base": "plate", "rarity": "Unique", "unique": "hidden", "item_level": 80,
        "sockets": 3, "runes": [], "affixes": [], "runes_hidden": true }
    ],
    "passive_nodes": ["hub_0", "ks_test_0", "hidden"], "tree_version": 7, "bonus_passive_points": 2,
    "ascendancy": { "aspect": "Frost", "nodes": ["d1", "d2"], "tree_version": 3 },
    "skills": [{ "skill": "split_shot", "supports": ["chain", "hidden"], "conduit_link": "" }],
    "gem_levels": { "split_shot": 20, "chain": 18, "dash": 5 },
    "hotbar": ["split_shot", "dash"], "main_skill": "split_shot",
    "skill_damage": { "split_shot": 1024 }
  }
}

meta/<league>.json

every 60 min

Usage shares for one league. share is a percent with one decimal.

{
  "schema": 1, "generated_at": "2026-11-10T21:46:48.258Z", "game_build": 64, "patch": "0.68.0",
  "league": "zz1-sc-trade", "window_days": 7, "heroes": 20,
  "classes": [{ "id": "Mage", "heroes": 7, "share": 35 }, { "id": "Ranger", "heroes": 6, "share": 30 }],
  "main_skills": [{ "class": "Mage", "skill": "split_shot", "heroes": 7,
                    "best_rupture_level": 500, "median_rupture_level": 88, "share": 35 }],
  "supports": [{ "skill": "split_shot", "support": "chain", "heroes": 20 }],
  "rupture_level_bands": { "75": 19, "500": 1 },
  "sample": { "heroes": 20, "note": "the published builds (top heroes per class)" },
  "ascendancies": [{ "id": "Mage/Ember", "heroes": 4, "share": 20 }],
  "keystones": [{ "id": "ks_test_0", "heroes": 11, "share": 55 }],
  "notables": [],
  "uniques": [{ "id": "hidden", "heroes": 3, "share": 100 }],
  "skills": [{ "id": "split_shot", "heroes": 20, "share": 100 }]
}

game-data/index.json and game-data/<file>.json

every patch

The game's own data for the current patch, with secrets masked: skills, supports, passive_tree, item_bases, affixes, runes, runewords, uniques, stat_text, scaling, classes, dragonling and more. index.json lists the files. Ids in the build and meta files point into these files.

The shape of index.json (written by my upload script each patch; the file list here is trimmed):

{
  "schema": 1, "game_build": 68, "patch": "0.68.0", "archive": "v1/static/b68",
  "uploaded_at": "...",
  "files": ["affixes.json", "item_bases.json", "passive_tree.json", "runes.json",
            "skills.json", "supports.json", "uniques.json"]
}

Each build's data is also kept in the folder named by archive (relative to the site root, so https://api.playbrokenoath.com/v1/static/b68/ for build 68), so older patches stay readable. Archive files never change and are cached for a day.

Refresh times

My server checks four times an hour, at minutes 3, 18, 33 and 48 (UTC). Each check takes a minute or two and writes:

FilesHow often
version.json, leagues.json, ladder/*Every check, so every 15 minutes
builds/*, meta/*Once an hour, on the first check at least 55 minutes after the last hourly run. Only builds that changed are uploaded again.
Measured Guardian numbersEvery 6 hours (the hourly run at 00, 06, 12 and 18 UTC), over the last day of fights
game-data/*With every patch

version.json carries the live values in refresh, ladder_updated_at and builds_updated_at. If I ever slow a refresh down, those numbers change first, so read them instead of hard coding mine.

Caching and polite polling

Examples

JavaScript (browser or Node 18+, as a module)

const base = "https://api.playbrokenoath.com/v1/";
const get = (path) => fetch(base + path).then((r) => r.json());
const version = await get("version.json");
for (const league of version.leagues) {
  const ladder = await get(`ladder/${league}.json`);
  const top = ladder.heroes.find((h) => h.has_build);
  if (!top) continue;
  const hero = await get(top.build);
  console.log(league, top.class, "RL", top.rupture_level, hero.build.main_skill);
}

curl

# what is live right now (leagues, refresh times, game data patch)
curl -s https://api.playbrokenoath.com/v1/version.json

# one league's ladder and meta (take the league id from version.json)
LEAGUE=eat-sc-trade
curl -s --compressed https://api.playbrokenoath.com/v1/ladder/$LEAGUE.json
curl -s --compressed https://api.playbrokenoath.com/v1/meta/$LEAGUE.json

# the Mage build list of that league, then one build from it
curl -s https://api.playbrokenoath.com/v1/builds/$LEAGUE/Mage.json
curl -s https://api.playbrokenoath.com/v1/builds/$LEAGUE/Mage/<hero>.json

# headers only: Cache-Control, ETag, Last-Modified
curl -sI https://api.playbrokenoath.com/v1/version.json

Who is in the files

The rules

It is an alpha

Fields can be added or change while the game is in alpha. Check schema before you parse and generated_at to know how fresh a file is, and ignore fields you do not know. Numbers, skills and the meta move with every patch, so please tell your users that too.

Talk to me

Made something, found a bug, or need a field that is not here? Come to the Discord: discord.gg/rU5NNFtTEF. I read every post, and I would love to see what you build.