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#
- Open the game.
- Open the Options menu(Hotkey p).
- Go to the Account tab.
- 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"
}
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | A required field is missing, or a field has the wrong type |
| 401 | Bad user_id or api_key |
| 403 | You do not own the thing you asked about |
| 404 | The record does not exist |
| 409 | Conflict, 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| skip_offline | bool | No | Skip the offline progress calculation. Faster, and avoids a preview you are going to ignore |
Returns:
state, the full save row. Wave fields aregame_wave,max_game_wave,max_game_wave_this_run, andcap_wave. Economy fields aregold,gold_gain_multiplier,total_gold_gained,total_gold_gained_this_run,total_damage,talent_currency, andtotal_talent_points. Location isstage_idandcurrent_level_world_stage_id. The wall iscastle_wall_current_health,castle_wall_max_health,wall_fragment_progress, andwall_fragment_warp_used_this_run. Run bookkeeping isrun_started,run_last_save,current_run_id,waves_skipped_this_run,total_run_enemies_killed, andwave_enemies_killed. Drop pacing islast_chest_wave,last_essence_wave, andlast_food_wave. Alsomax_hero_levels,total_missions_completed, andlast_popup_chest_claim_at.offline_progress_rewards, a preview of what is waiting. Read requests never bank it.stacks, current run stack counts for Xylok and Cydra.tutorial_progress, keyed by tutorial ID.mount_campaign_rewards, the bonuses earned from campaign stars.resonance_chain, your active resonance data.spotlight_worlds, the current spotlight rotation.stage_run_count_last_7d, how many runs you have done on your current stage in the last week.
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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| hero_name | string | No | Limit 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| user_mine_id | int | Yes | Which 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| user_mine_id | int | Yes | Which 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#
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| missions_daily_id | int | Yes | The 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| template_id | int | Yes | Mission template ID |
| user_hero_ids | array | Yes | Heroes to send. Cannot be empty |
| duration_seconds | int | No | Preview 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | No | Include for personal rank context |
| api_key | string | No | Include 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.
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | int | Yes | Your user ID |
| api_key | string | Yes | Your read-only key |
| target_user_id | int | Yes | The 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.
| Field | Type | Required | Description |
|---|---|---|---|
| limit | int | No | 1 to 50, default 10 |
| version | string | No | Fetch 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.
| Endpoint | Returns |
|---|---|
get_hero_data.php | Every hero definition, with abilities and traits |
get_mounts.php | Every mount definition and base stats |
get_mount_elements.php | Mount elements and their strength and weakness relationships |
get_enemies.php | Enemy definitions, stats, and spawn weights |
get_worlds_and_stages.php | World and stage hierarchy with objectives |
get_campaign_stages.php | Mount campaign stage definitions |
get_chests_and_currency.php | Chest 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.