Read-only endpoints
The JSON and WebSocket endpoints the Basement app reads, covering arenas, rounds, leaderboards, crew form and B3 reserves. Unversioned and may change.
These are the endpoints the Basement app itself reads to show arenas, rounds, boards and the live match. Anyone can call them with a plain GET from a server, without a key or a sign-in. They aren't a supported API.
None of these endpoints are versioned or covered by any stability promise. Fields, shapes and URLs can change or disappear with any release, without notice. Build so that a missing field doesn't break you.
Endpoints
All paths are on https://basement.fun. Every response is JSON except the card images.
| Endpoint | What you get | Cached for |
|---|---|---|
GET /api/time | { now }, the server clock in Unix milliseconds | Not cached |
GET /api/arenas | { arenas }: all six arenas, each with its current and next round, viewers, the last 10 results and live win chance | 1 second, on the server |
GET /api/arenas?slim=1 | The same, without locks, reveals and recent results | 1 second, on the server |
GET /api/arenas/:id | One arena, same shape as an item above | Not cached |
GET /api/arenas/:id/rounds/:n | { round, drops } for any round. round.reveal appears once it has a result. | Not cached |
GET /api/leaderboard?period=day¤cy=play | The top 20 players for a period and currency | Revalidate with ETag |
GET /api/cast | Form for all 15 crew members | 15 seconds |
GET /og/:arena.png and /og/:arena/:n.png | Preview card images, see links and embeds | 30 seconds open, a week finished |
GET /api/b3 | Whether real B3 is on, withdrawal limits and the terms version | Not cached |
GET /api/b3/reserves | The vault balance against what Basement owes players | 30 seconds |
GET /api/push/key | The public key for Web Push subscriptions | 5 minutes |
:id and :arena are arena ids: rush-hour, clash, knockout, broadside, skyball or turf-war. :n is a round number.
Arenas and rounds
GET /api/arenas/knockout during a live match, trimmed:
json{ "arena": "knockout", "now": 1791812159803, "round": { "arena": "knockout", "n": 12366, "kernelVersion": "clash-4-ring", "phase": "live", "announcedAt": 1791812113263, "lineupAt": 1791812147413, "liveAt": 1791812155583, "maxEndAt": 1791812190583, "entrants": [ { "index": 0, "cast": { "id": "toxic", "name": "Toxic", "skin": "hazmat" }, "color": "#ff4d6d" }, { "index": 1, "cast": { "id": "orbit", "name": "Orbit", "skin": "astronaut" }, "color": "#3d9bff" } ], "multiplierBps": 19000, "commit": "4c640dd8b99ba822c04371fc77aaa0b01e3e7b6da8e30c06fe73e1060018a079" }, "next": { "n": 12367, "phase": "upcoming", "commit": "ea30e496780183dba7fc23406c0cf06aa4a6383e68eae1985b1672f90cef3540" }, "viewers": 1, "recent": [{ "n": 12365, "winner": 1, "winnerName": "Orbit", "color": "#3d9bff" }], "chance": [0.428, 0.572]}
- Times are Unix milliseconds on the server clock. Use
GET /api/timeor thenowfield to correct for your own clock. phaserunsupcoming,lineup,live,result. A round takes picks fromannouncedAtuntilliveAt, so the round taking picks isnextwhile a match is live or showing its result, androundduring the lineup.nextisnullduring the lineup.maxEndAtis the latest the match can end. The real end isresult.endedAtonce there's a result.entrantsare the sides you can pick, byindex. In Turf War each entrant is a squad, with its crew inmembers.multiplierBpsis the fixed payout in basis points, so19000pays 1.9x, stake included.result.winneris an entrant index, ornullwhen the round was void and every pick refunded.placingslists every entrant, best first.result.voidReasonappears only on a void round and says why:kernel-changed(a game update landed while the round was locked or live),ledger-unreachable(the round couldn't be locked in the ledger),timeout(time ran out with nobody ahead) orother(anything else, such as a game error).chanceis the live win chance per entrant from what's visible on the stage. It appears only while a match is live.viewerscounts open connections that have this arena on screen right now.unsettledappears only while a finished round is still waiting for the ledger to settle it:{ count, n, endedAt }, with how many rounds are waiting and the oldest one's number and end time. It's normally absent.
/api/arenas/:id/rounds/:n gives one round in the same shape plus its drops, with reveal once it's over. A finished round never changes, so cache it for good. Verify a round walks through a full response and the Drop fields.
Leaderboards and crew form
GET /api/leaderboard takes two parameters:
period:day(since 00:00 UTC),week(since Monday 00:00 UTC) orstreak(today's longest runs of rounds that paid back more than was staked)currency:playorb3
json{ "period": "week", "currency": "play", "rows": [{ "rank": 1, "handle": "RowdyCorgi37", "net": 790 }], "me": null}
That's trimmed, with the handle replaced. net is payout minus stake over settled picks in the period, and km, when present, counts the player's Kingmaker moments. The streak board has best in place of net. me is always null without a session. Rows can include players at zero or below; the app shows only rows above zero. Boards recompute at most every 30 seconds. Send the ETag back as If-None-Match and an unchanged board costs you a 304. More on how boards work in leaderboards.
GET /api/cast gives { cast: [...] }, one entry per crew member, from settled rounds with voids left out. One entry, trimmed:
json{ "id": "valk", "wins": 4125, "losses": 8339, "par": 4114.3, "streak": 1, "best": "clash", "arenas": { "knockout": [681, 706], "skyball": [515, 512] }, "recent": [{ "arena": "knockout", "n": 10556, "place": 1, "at": 1791745169119 }]}
par is how many wins an even share would have given, arenas holds wins and losses per arena, and recent has the last 8 results, newest first. In Turf War place is the squad's place. The crew are covered in the crew.
Real B3 status and reserves
GET /api/b3 says whether real B3 is on (enabled), whether withdrawals are open (withdrawals), the current termsVersion, and the withdrawal limits minWithdraw, dailyWithdrawLimit and reverifyAbove, all in whole B3. Its account fields are empty without a session. allowedHere describes where the request came from, so a server's answer says nothing about any player's region.
GET /api/b3/reserves compares the B3 in the vault with what Basement owes players. Every field is explained in where your B3 is held.
Live socket
wss://basement.fun/ws/:arena streams one arena as JSON text messages, each with a type in t. The first message is always snap, with the current and next round, recent frames and events, Drops, recent results, viewers and recent activity. After that:
t | When it arrives |
|---|---|
round | A round changes phase. Carries round and next. |
frames | Match frames, about 10 a second in 5 batches a second while live. Outside a match, one held pose about once a second. |
events | Moments in the match, like a hit, a goal, a knockout or the finish |
drop | A Drop was accepted. It lands 600 ms later. |
pools | The crowd split of a round taking picks changed |
activity | New picks, wins and Drops across all arenas, as the app's ticker shows them |
viewers | The arena's viewer count changed |
react | Reaction counts from people watching |
kingmaker | A Drop decided the round, named at the finish |
The frame format differs per game and isn't documented. A frames batch can repeat ticks you already have from snap, so skip ticks you hold. Your connection counts as a viewer and gets frames until it sends {"t":"focus","on":false}. To check the connection, send {"t":"ping","id":1} and you get a pong with the same id. Don't send reactions; they're for people watching.
An unknown arena answers 404, a request without a WebSocket upgrade 426, and an arena that's still starting 503 with retry-after: 1.
Errors and rate limits
API errors are JSON with an HTTP status:
json{ "error": "bad_request", "message": "Unknown round." }
error | Status | From a read, it means |
|---|---|---|
bad_request | 400 | Unknown arena, unknown round or unknown path |
rate_limited | 429 | Too many requests from your network. Wait for retry-after. |
unavailable | 503 | Something failed on the server. Try again shortly. |
The other codes come from app actions, never from the reads on this page: unauthorized (401), insufficient_funds (402), betting_closed, round_not_open, drops_closed and limit_reached (409), and geo_blocked (451).
An unknown round is a 400 from the API and a 404 from /og/. Card and socket errors are plain text.
Limits count per network, meaning one IPv4 address or one IPv6 /64, over 10-second windows:
| What | Limit per 10 seconds | retry-after |
|---|---|---|
Reads: /api/*, card images, arena and replay pages | 300 | 2 seconds |
| Leaderboard reads, which also count as reads | 20 | 5 seconds |
| New socket connections | 40 | 2 seconds |
Poll a running round every few seconds at most, or hold one socket per arena instead. Finished rounds and finished cards never change, so fetch them once.