Contents
No match.

Read-only API for external tools, spreadsheets, Discord bots, and anything else you want to build against your own account.

Last verified against the live backend on 2026-08-21.

What you can and cannot do#

Your read key can fetch your own data and the public data below. It cannot change anything.

A read request may calculate and show you offline progress, but it never persists rewards, stacks, timestamps, or any other state. Nothing you do here can affect your save.

Getting your API key#

  1. Open the game.
  2. Open the Options menu(Hotkey p).
  3. Go to the Account tab.
  4. Your read-only key is in the API Key field with a Copy button next to it.

Guests and full accounts both get a key. Anyone holding it can read your account.

Base URL#

https://api.formationbuilderidle.com

All paths below are relative to that.

Request format#

Every endpoint takes a POST with a JSON body. Even endpoints with no arguments need the body, because that is where the credentials live.

{
  "user_id": 999,
  "api_key": "your_read_only_api_key_here"
}

No headers are required beyond Content-Type: application/json.

Response format#

Success always includes ok:

{
  "ok": true,
  "heroes": [ ... ]
}

Errors always include error:

{
  "error": "Unauthorized"
}
StatusMeaning
200Success
400A required field is missing, or a field has the wrong type
401Bad user_id or api_key
403You do not own the thing you asked about
404The record does not exist
409Conflict, usually a cached profile being regenerated. Retry shortly

Rate limits#

30 requests per second per IP, with a burst allowance of 50. Go over it and nginx drops the connection with no response at all instead of returning a status code, so your HTTP client will report a connection reset rather than an error body. Auth endpoints such as login and register are capped much harder at 5 per minute, but you have no reason to hit those.

That ceiling is generous. Polling once a second is fine. If you are hitting the limit, you are almost certainly looping when you meant to sleep.


Your account#

Player state#

get_user_state.php#

The big one. Current run, wave progress, currencies, and the derived data the game screen needs.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
skip_offlineboolNoSkip the offline progress calculation. Faster, and avoids a preview you are going to ignore

Returns:

Large numbers come back as scientific-notation strings, not floats. Parse them as strings. Values well past 1e308 are normal in this game and will silently become inf if you push them through a float.

get_user_heroes.php#

Your hero roster.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key

Returns heroes, an array of {hero_id, level, user_hero_id}.

get_user_gear.php#

Every gear piece across every hero, plus the upgrade cost table.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
hero_namestringNoLimit results to one hero by name

Returns gear and gear_upgrade_defs.

get_user_talents.php#

Returns talents, your talent levels.

get_inventory.php#

Chests, currencies, and mount currencies with their names, icons, and descriptions joined in.

Call it with the standard read-only request:

curl -X POST https://api.formationbuilderidle.com/get_inventory.php \
  -H 'Content-Type: application/json' \
  -d '{"user_id":999,"api_key":"your_read_only_api_key_here"}'

Returns inventory. Each row has inventory_item_type_id (1 is chests, 2 and 4 are currencies), item_id, amount, chest_name or currency_name, icon_path, and description. The Tailor's Chest description is rewritten server-side with your actual scaled reward amounts, so it will not match the static chest table.

The response also includes header, a stable summary of every balance shown at the top of the in-game inventory. Every field is always present and a currency without an inventory row returns 0:

{
  "ok": true,
  "header": {
    "diamonds": 123,
    "animal_food": 456,
    "gear_essence": 789,
    "otherworldly_essence": 0,
    "prestige_warp": 0,
    "hay_bales": 0,
    "oats": 0,
    "pet_treats": 0,
    "wall_fragments": 0
  },
  "inventory": [ ... ]
}

get_user_save_formation.php#

Returns formation_data, your manual save slots. Slots are world-specific and stored as slot-to-hero maps.

get_user_stage_achievements.php#

Returns user_stage_achievements, your per-stage completion records.

get_user_seer_level.php#

Returns seer_data with seer_level, xp_into_level, xp_to_next, and progress_pct, plus seer_level_rewards. Seer level is calculated server-side, so this is the authoritative number.

tutorial/get_progress.php#

Returns tutorial_progress, keyed by tutorial ID.


Mounts#

get_user_mounts.php#

Returns mounts, your owned mounts with stats and prestige levels.

get_user_mount_teams.php#

Returns mounts, your team assignments.

get_user_mount_formations.php#

Returns formations, keyed by terrain.

get_mount_riders.php#

Returns riders_by_mount, hero-to-mount assignments keyed by mount.

get_mount_campaign_rewards.php#

Campaign star milestones and the bonuses they grant.

get_achievement_mount_bonuses.php#

Mount stat bonuses earned from main-game campaign progress. Stages 38, 40, and 71 are the milestones that matter.

get_combat_power_bonus.php#

Your total mount combat power multiplier. This is the same calculation the server uses in real battles, so use it rather than deriving your own. It is 5% per Head Start level, 1% per Seer level, 3% per Mount Fury level, and 4% per Warhorn level.


Tower of Domination#

tower_get_state.php#

Returns current_floor, highest_floor, weekly_highest_floor, highest_boss_floor_beaten, last_reset, cooldown_duration, normal_cooldown_duration, is_catchup_cooldown, and the upcoming floor previews.


Mining#

mine/mine_state.php#

Everything the mining screen needs in one call.

Returns mine_defs, mines, resources, states, inventory, milestone_info, user_mine_assignments, user_mine_machine_assignments, mine_resource_cost_def, mine_player_rewards, mine_rates, and hero_mine_contributions.

mine/mine_rate.php#

Live production rate for one mine.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
user_mine_idintYesWhich mine to query

Returns server_ts plus the rate breakdown: resource_def_id, level, xp, next_xp, xp_per_sec, actions_per_sec, yield_per_action, base_time_s, time_modifier, time_cap_reached, effective_action_time_s, and yield_per_sec. It also includes an inventory snapshot for that mine's resources. Read-key calls compute the rate without consuming resources.

mine/mine_rewards_state.php#

Mine reward upgrade progress.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
user_mine_idintYesWhich mine to query

Returns rewards, each with reward_def_id, key_name, display_name, description, current_level, current_effect, next_level, next_effect, and next_recipe.


Missions#

missions_bootstrap.php#

The whole mission screen in one request. Prefer this over calling missions_list.php and missions_my_list.php separately.

Returns available, window, active, server_time, total_completed_missions, and hero_availability.

missions_list.php#

Returns missions and window, the offers in the current daily window. The window rolls over on America/Denver time, not UTC.

missions_my_list.php#

Returns missions and server_time, your started and finished mission instances. Compare planned end times against server_time rather than your own clock.

missions_template_detail.php#

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
missions_daily_idintYesThe daily offer ID, from missions_list.php

Note this takes the daily offer ID, not the template ID.

missions_preview.php#

Success chance and reward preview for a hero selection, without starting anything.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
template_idintYesMission template ID
user_hero_idsarrayYesHeroes to send. Cannot be empty
duration_secondsintNoPreview an early-end at this elapsed time

get_total_completed_missions.php#

Returns total_completed_missions.


World mastery#

world_mastery/get_mastery_data.php#

Full mastery snapshot: 11 worlds by 5 tiers, crowns, MP upgrades, and World Essence balances.

Returns tiers keyed by world, progress, crowns, mp_balance, mp_upgrades, we_balances keyed by world, and tailors_chest_we_cost.

world_mastery/get_tailors_chest_info.php#

Returns we_cost (your discounted price), scaling_multiplier, and drop_table. Currency entries include both raw min/max and your personal scaled_min/scaled_max.


Public data#

Leaderboards#

leaderboard/get_all_leaderboards.php#

No authentication needed. Send user_id and api_key anyway and your own rank gets included, and your rows get flagged.

FieldTypeRequiredDescription
user_idintNoInclude for personal rank context
api_keystringNoInclude for personal rank context

Returns categories, keyed by category name. Categories are talent_points, mount_power, seer_level, campaign_progress, gear_score, max_wave, mine_progress, tower_floor, and missions_completed.

Each category has category, category_name, leaderboard (top 100 with rank, user_id, username, score_display, is_current_user), total_players, current_user, last_updated, and last_updated_relative.

Scores are recalculated on a schedule, not live. last_updated tells you how stale the board is.

profile/get_public_player_profile.php#

Another player's public profile, as shown when you click a leaderboard entry.

FieldTypeRequiredDescription
user_idintYesYour user ID
api_keystringYesYour read-only key
target_user_idintYesThe player to look up

Returns profile, generated_at, and stale. The profile has username, leaderboard_scores, rough_stats, heroes, mounts, campaign, and tower.

Only ranked players have profiles. Anyone else returns 404. Snapshots are cached for one hour, so generated_at can be up to an hour behind and re-requesting will not refresh it. A 409 means the snapshot is being rebuilt right now, so wait a second and retry.

Patch notes#

patch_notes/get_patch_notes.php#

No authentication needed at all.

FieldTypeRequiredDescription
limitintNo1 to 50, default 10
versionstringNoFetch one specific version. Returns 404 if it does not exist or is unpublished

Returns patch_notes, each with version, title, sections, and created_at, newest first.


Game data#

Static reference tables. They change only when the game updates, so cache them locally instead of refetching on every run.

All of these take user_id and api_key and nothing else.

EndpointReturns
get_hero_data.phpEvery hero definition, with abilities and traits
get_mounts.phpEvery mount definition and base stats
get_mount_elements.phpMount elements and their strength and weakness relationships
get_enemies.phpEnemy definitions, stats, and spawn weights
get_worlds_and_stages.phpWorld and stage hierarchy with objectives
get_campaign_stages.phpMount campaign stage definitions
get_chests_and_currency.phpChest types and currency definitions

Examples#

curl#

curl -X POST https://api.formationbuilderidle.com/get_user_state.php \
  -H "Content-Type: application/json" \
  -d '{"user_id": 999, "api_key": "your_read_only_api_key", "skip_offline": true}'

Python#

import requests

BASE = "https://api.formationbuilderidle.com"
AUTH = {"user_id": 999, "api_key": "your_read_only_api_key"}

def call(endpoint, **params):
    r = requests.post(f"{BASE}/{endpoint}", json={**AUTH, **params}, timeout=30)
    data = r.json()
    if not data.get("ok"):
        raise RuntimeError(data.get("error", f"HTTP {r.status_code}"))
    return data

state = call("get_user_state.php", skip_offline=True)["state"]
print("Wave:", state["game_wave"], "of best", state["max_game_wave"])
print("Gold:", state["gold"])

for hero in call("get_user_heroes.php")["heroes"]:
    print(hero["hero_id"], "level", hero["level"])

JavaScript#

const BASE = 'https://api.formationbuilderidle.com';
const AUTH = { user_id: 999, api_key: 'your_read_only_api_key' };

async function call(endpoint, params = {}) {
  const res = await fetch(`${BASE}/${endpoint}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...AUTH, ...params })
  });
  const data = await res.json();
  if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`);
  return data;
}

const { state } = await call('get_user_state.php', { skip_offline: true });
console.log(`Wave ${state.game_wave}, gold ${state.gold}`);

Leaderboard without an account#

curl -X POST https://api.formationbuilderidle.com/leaderboard/get_all_leaderboards.php \
  -H "Content-Type: application/json" \
  -d '{}'

Gotchas#

Big numbers are strings. Gold, damage, and wall health can exceed what a 64-bit float holds. They arrive as scientific-notation strings. Keep them as strings, or use a decimal library. Do not call parseFloat and hope.

Offline progress in get_user_state.php is a preview. Reading it does not claim it. If you are building a stats dashboard, pass skip_offline: true and skip the calculation entirely.

Mission windows are Denver time. The daily offer window uses America/Denver, so it does not line up with UTC midnight.

Profiles are cached an hour. generated_at on a public profile tells you how old it is. There is no way to force a refresh.

Rate limit failures look like network errors. Exceeding 30 requests per second gets your connection dropped without a response body, not a 429.

Endpoint paths include the .php. Some live in subdirectories: mine/, missions at the root, world_mastery/, leaderboard/, profile/, patch_notes/, and tutorial/.

Support#

Ask in Discord. The invite is in the game under Options, Game tab. Bug reports about this document are welcome, especially if an endpoint returns something other than what is written here.