Introduction
This documentation refers to version 1.0 of Five Minute Leaderboards.
A leaderboard collects scores and keeps them ranked, either for all time or on a repeating cycle such as a day, a week or a month. You create each board in the dashboard or from your server and choose how a new score combines with the stored one, whether higher or lower is better, when the board starts over, and whether clients may submit scores themselves. Five Minutes stores the records, ranks them, keeps the history of past cycles and gives you tools to remove bad records.
There are two ways in:
- Your server submits scores, reads boards and removes records over the Five Minute Server API (HTTPS and JSON), and can create, change and delete boards. It addresses a board by its GUID id.
- Logged-in clients on protocol 2.0 (signed in, or logged in the legacy way with a self-declared id) submit the connected user's own score and read boards over the connection they already hold. They address a board by its key.
The npm packages 2.0 have the client calls: submitScore(), getLeaderboard() and getLeaderboardRecord() (see npm packages 2.0). The Unity asset has none: it speaks protocol 1.7 or earlier (asset 1.20 speaks protocol 1.6, asset 1.23 speaks protocol 1.7; see Client libraries), and an app built on it reaches leaderboards through your server.
Requirements
- A board, created in the dashboard or by your server (see Creating a board).
- For your server: an API key of the application holding
Application.Scores.Read("Leaderboards: read") to read records and boards, andApplication.Scores.Write("Leaderboards: write") to submit and remove records. Creating, changing and deleting boards needsApplication.Leaderboards.Write("Leaderboards: manage boards"), whichApplication.Scores.Writedoes not include.Application.Admin.FullAccess("Full access") grants all three. - For clients: protocol 2.0 and a completed login. A score from a client is accepted only on boards with Allow client submission turned on. Sign users in (see Five Minute Player Authentication) so that a client cannot claim another user's unique user id.
Quick facts for AI agents
- Covers: boards, scoring operators, cycles, ranking, the 5 record and cycle routes of the Server API and the 3 client message pairs. The 5 routes that create, read, change and delete boards are in the Server API reference (Leaderboard definitions).
- Build with: your server calls the routes below over HTTPS; the client sends the messages below through a Five Minutes client library, never through its own implementation of the protocol. In JavaScript, the npm packages 2.0:
submitScore(key, score, options?),getLeaderboard(key, options?)andgetLeaderboardRecord(key, cycleId?)(see npm packages 2.0). The Unity asset has no leaderboard calls; for an app built on it, submit and read through your server. - Your server: base URL
https://signalr.fiveminutes.cloud/api/v1.GET /leaderboards/{leaderboardId}/records,GET /leaderboards/{leaderboardId}/records/{ownerId}andGET /leaderboards/{leaderboardId}/cyclesneedApplication.Scores.Read;POST /leaderboards/{leaderboardId}/recordsandDELETE /leaderboards/{leaderboardId}/records/{ownerId}needApplication.Scores.Write. Boards:GET /leaderboardsandGET /leaderboards/{leaderboardId}needApplication.Scores.Read;POST /leaderboards,PUT /leaderboards/{leaderboardId}andDELETE /leaderboards/{leaderboardId}needApplication.Leaderboards.Write. Write does not imply Read, and neither write scope includes the other. - Server auth:
POST /api/v1/api-clients/authenticatewith{"applicationId":"<application id>","clientSecret":"<API key secret>"}returns{"token","expiresInSeconds":600,"scopes"}; sendAuthorization: Bearer <token>. - Client: protocol 2.0; messages
ClientSubmitScoreRequest,ClientGetLeaderboardRequest,ClientGetLeaderboardRecordRequestand their typed responses; send only after ServerWelcome; a new MessageId per message; match replies on ResponseToMessageId. - Identifiers:
leaderboardIdis a GUID and the only way REST addresses a board (a key in its place answers404); copy it from the last path segment of the board's dashboard page, or read it asidfromGET /api/v1/leaderboards. Board key: 1-128 characters, case-sensitive, used only on the wire.ownerId: 1-128 characters; for a user's record use the unique user id (UniqueUserId). Clients never receive another user's unique user id: on the wire only the user's own record carriesOwnerId(withIsSelftrue), records of users carryOwnerDisplayIdandOwnerName, and records of other owners, such as a clan, carry none of the three, so identify them byMetadata. An owner id your server chooses for a clan or team must be one no user can log in with, such asclan_followed by 32 random hexadecimal characters, and boards of such owners are shown fromMetadataonly (see Concepts).cycleId:all,2026,2026-09,2026-W40,2026-09-29,2026-09-29T14,2026-09-29T14:30or2026-09-29T14:30:15(UTC). - Limits:
limit1-100, default 50; ranks are 0-based;rankisnull(REST) or-1(wire) on a write while the board's ranking is cold, and on single reads of closed cycles; each submitted score within ±2^53 and at most the board's Max-score bound; wireMetadataat most 4096 characters; a group view holds at most 100 users including the user; client submissions: the plan's "Messages Per Second Per User" × 10 per 10-second window per user (10 on Free and Subscription); edge limit 5 requests per second per IP address. - Retries:
Idempotency-Keyheader (1-128 characters) onPOST .../records, and onPOST /leaderboardswhen creating a board (see the Server API's Idempotency and retries). A keyed record write is applied at most once for 24 hours, whichever region serves the retry, and a retry gets the first write's result; retry409,429,500,502and503with the same key and body (see Exactly-once totals). A record write without a key is a new write every time. GET, PUT and DELETE are safe to repeat. On the wire the MessageId is the key: resend an unansweredClientSubmitScoreRequestunchanged, with the same MessageId (with the npm packages,submitScore(key, score, { MessageId: error.messageId }); see After a lost response). - Scoring rules: a board's operator, start score, sort order and reset cadence are locked while it holds a record in any cycle; changing one is refused (
409onPUT /leaderboards/{leaderboardId}, the same in the dashboard), while the other settings stay editable (see Locked scoring rules). - Regions: records are shared by every region, and every region's ranking of the current cycle follows the writes made through all regions: a write through the same region is ranked at once, a write through another region normally within 1 to 2 seconds (see Regions).
- Does not exist: a board key in REST paths; changing a board's key; changing a board's operator, start score, sort order or reset cadence while it holds records; a start score on a Best or Set board;
friendsOf, group or around-me parameters over REST (useownerIds); ordering by subscore; writes into a past cycle; a batch endpoint; a client writing any record other than its own user's; leaderboard calls in the Unity asset. - Related: Server API, wire protocol, player authentication, API contracts.
Documentation set
- Five Minute Server API 1.0: hosts, API keys, the server token, error bodies, rate limits and idempotency for every server call.
- Five Minute wire protocol 2.0: how a client connects, logs in and exchanges messages.
- Five Minute Player Authentication 1.0: who the user is, and where the unique user id comes from.
- Five Minute API Contracts 2.0: the field-level reference of every wire message.
- Five Minute Leaderboards 1.0 (this document): boards, scoring, cycles, ranking, and the leaderboard routes and messages.
Concepts
Board. One ranked list with one set of rules, belonging to one application. A board has a key and an id.
Key. The name clients use for a board, such as weekly_arena. You choose it when you create the board (dashboard: "Stable id"). It is 1-128 characters and cannot be changed. Clients must send it exactly, with the same letter case. It must be unique within the application regardless of letter case: when weekly_arena exists, creating Weekly_Arena is refused, in the dashboard and over the Server API.
Board id. A GUID that Five Minutes generates for a board. Your server's REST calls use it as {leaderboardId}; the examples write it as <leaderboard id>. Find it in the dashboard: open the board; the id is the last path segment of the page address (.../leaderboards/<leaderboard id>). Your server can also read it as id from GET /api/v1/leaderboards, which lists every board with its key. Store it in your server's configuration next to the key.
Owner id. The text id a record belongs to, 1-128 characters. For a user's record it is the unique user id (UniqueUserId, see Five Minute Player Authentication): clients always write under that id, and group views look users up by it. Your server can also write records for other owners, such as a clan or a team, under ids you choose. For ids you choose:
- Do not use two ids that differ only in letter case, such as
Clan-Aandclan-a; they may be treated as the same owner. - Do not use
/in an id. Your server's reads and removals carry the owner id in the URL path, and an id containing/cannot be addressed there. - Choose ids no user can log in with, and never send them to clients. While Require authenticated sessions is off, a client can log in with any unique user id it declares, and a login with an id no user has yet creates a user with that id. Records under that id then carry that user's display id and the name whoever logged in chose. Use an id nobody can guess, for example
clan_followed by 32 random hexadecimal characters, or by an HMAC of your own clan id under a key only your server holds. When clients need a handle for the clan, put a separate one into the metadata.
Owner ids stay between Five Minutes and your server. Your server's reads return the owner id of every record. A client gets an owner id only on its own user's record; every other record reaches it without one:
- A record whose owner id is the unique user id of a user of the application carries that user's display id and name (
OwnerDisplayId,OwnerName), as they are now rather than when the score was submitted. The display id is what your app shows and uses to address the user, for example in a whisper. - A record of any other owner, such as a clan, carries no owner id, display id or name. Put what clients should show, such as the clan's name, into the record's metadata, and have clients show the rows of a board your server writes for such owners from the metadata only, whatever the owner fields hold.
Record. One owner's entry on one board in one cycle: a score, a subscore and metadata. An owner has at most one record per board and cycle.
Score. A 64-bit whole number. The board's operator decides how a submitted score combines with the stored one.
Subscore. A second 64-bit whole number stored and returned with the record, for example the time a run took. It is display data only: it is never used to order records or to decide whether a Best score improved. A write that omits it stores 0.
Metadata. Free-form text stored with the record and returned unchanged, such as a display name or a replay reference. Five Minutes never interprets it. Clients can send at most 4096 characters. Your server's writes have no enforced limit; keep metadata to 4096 characters or less and make it valid JSON, because clients read it and the dashboard shows it as JSON. Every user who reads the board receives it, so never put a unique user id (UniqueUserId) into it: a unique user id must never be shown or sent to other users (see Five Minute Player Authentication). To show who a record belongs to, store the user's name or display id.
Operator. The board's rule for combining scores: Best, Set, Increment or Decrement. See Scoring rules.
Cycle. One period of a board, named by a cycle id such as 2026-W40. An all-time board has a single cycle, all. Writes always land in the current cycle; earlier cycles are kept as read-only history. See Cycles.
Rank. A record's zero-based position: the best record has rank 0. The dashboard shows ranks one-based (#1).
Client submission. A score sent by a client for its own user. It is accepted only on boards that allow it, and is limited per user.
Personal group. A named list of users that one user keeps, for example their friends. The client creates and edits it with ClientAddUsersToPersonalGroupRequest, and your server with the personal group endpoints. A client can view a board narrowed to one of its own user's personal groups.
Creating a board
Create and manage boards in the dashboard: open the application, go to Leaderboards and choose Create leaderboard. After creating a board, copy its id (the last path segment of the board's page address) into your server's configuration, and its key into your app.
Your server can do the same over the Server API: POST /api/v1/leaderboards creates a board and answers with its id, GET /api/v1/leaderboards lists the boards with their ids and keys, PUT /api/v1/leaderboards/{leaderboardId} changes the settings and DELETE /api/v1/leaderboards/{leaderboardId} deletes the board. Creating, changing and deleting need Application.Leaderboards.Write, reading needs Application.Scores.Read. The requests, the answers and the errors are in Leaderboard definitions; the same rules apply as in the dashboard, Locked scoring rules among them.
Board settings
| Setting (dashboard label) | Server API property | Values | Default | Change later? | Meaning |
|---|---|---|---|---|---|
| Key ("Stable id") | key |
1-128 characters; unique in the application regardless of letter case; matched case-sensitively | Derived from the display name (dashboard); required (Server API) | No | The name clients use for the board. |
| Title ("Display name") | title |
Text, optional | Empty | Yes | The name shown in the dashboard. |
| Sort direction | sortOrder |
"Higher is better" (Descending) or "Lower is better" (Ascending) |
Higher is better | Only while the board holds no records (Locked scoring rules) | Whether a bigger or a smaller score ranks first. |
| Scoring operator | operator |
Best, Set, Increment, Decrement (shown as "Subtract") | Best | Only while the board holds no records (Locked scoring rules) | How a submitted score combines with the stored one. |
| Start score | startScore |
A whole number between -2^53 and 2^53, not above the Max-score bound; only Increment and Decrement boards take one, any other board keeps 0 | 0 | Only while the board holds no records (Locked scoring rules) | The score every owner starts each cycle with on an Increment or Decrement board: the first write of a cycle adds to it or subtracts from it. |
| Reset cadence | resetCadence |
All-time (Never), Yearly, Monthly, Weekly, Daily; Hourly, Every minute (Minutely) and Every second (Secondly) under "Show hourly and finer cadences" |
All-time | Only while the board holds no records (Locked scoring rules) | When a new, empty cycle starts. |
| Allow client submission | allowClientSubmission |
On or off (true, false) |
Off | Yes | Whether clients may submit their own user's score. Your server can always write. |
| Max-score bound | maxScore |
A whole number, 0 or more, or empty (null) for no bound |
Empty | Yes | The highest value one submission may carry. |
The finer cadences are meant for short live events and for testing. Most apps use All-time, Daily, Weekly or Monthly.
Allow client submission. When it is off, only your server writes scores, and a client cannot forge one. When it is on, any client can post any number for its own user: keep the Max-score bound tight and do not pay out rewards from such a board. A client can only ever write its own user's record; records of any other owner id, such as a clan or a team, can only be written by your server.
Max-score bound. It stops the most common way a client-writable board gets ruined. Set it to the highest value your app can legitimately produce in one submission. It applies to writes from your server too. See Negative values, bounds and totals for exactly what it checks.
Locked scoring rules
A board's scoring rules are its Scoring operator, Start score, Sort direction and Reset cadence (operator, startScore, sortOrder and resetCadence). They are locked while the board holds a record in any cycle, closed cycles included, because the stored scores were combined, ranked and grouped into cycles by them. The lock is the same in the dashboard and for your server's PUT /api/v1/leaderboards/{leaderboardId}.
- A change of any of the four on a board that holds records is refused, and nothing is saved: the dashboard shows the reason, and your server gets
409with it as a JSON string. Title, Allow client submission and the Max-score bound can still be changed. Values sent unchanged are not a change, so your server reads the board and sends the four rules back as they are; astartScoreleft out of thePUTbody keeps the board's start score. - The reason names the rules the change touches, in the order operator, start score, sort order, reset cadence, for example
operator and sort orderorstart score:
This leaderboard holds records, so its scoring rules are locked: its operator and sort order cannot be changed, because the stored scores were combined, ranked and grouped into cycles by the current rules. Other settings can still be changed. To use different scoring rules, create a new leaderboard, or first delete this one or remove all of its records in every cycle.
- To use different rules, create a new board with them. Or empty this board first: remove every record in every cycle (your server can remove records of closed cycles, the dashboard only those of the current cycle; see Moderation and recovery), or delete the board and create it again with the same key. A board created again gets a new id and starts empty; the deleted board's records are erased (see Deleting a board).
- On the Configuration tab the four are greyed out while the board holds records in any cycle, closed cycles included; Title, Allow client submission and the Max-score bound can still be saved there. When the page cannot tell whether the board holds records, for example during a brief disruption, or the board receives its first record after the page was opened, the tab offers the four, and saving a change of them is refused with the reason above (or the one below, when the check cannot be made). After a refusal with the reason above, the page reloads and greys them out.
- Whether the board holds records is checked only when a change touches one of the four rules. If that check cannot be made, the change is refused and nothing is saved; your server gets
503withRetry-After: 5and the detail"Whether this leaderboard holds records could not be checked, so its scoring rules were not changed and nothing was saved. Retry shortly; a change that keeps the operator, start score, sort order and reset cadence as they are does not need this check.". Retry it. - On a board without records the rules can change. Set them before scores arrive: a changed rule takes effect within about 2 minutes (see When changes take effect), and a score written in between is scored with the previous rules and then held under the new ones.
Dashboard roles
| Action | Roles |
|---|---|
| View boards, standings and cycles | Owner, Organization admin, Organization viewer, Application admin, Application viewer, Application moderator |
| Remove records | Owner, Organization admin, Application admin, Application moderator |
| Create, edit and delete boards | Owner, Organization admin, Application admin |
Your server needs no role: its API key's scopes decide. Application.Scores.Read reads boards, Application.Leaderboards.Write creates, changes and deletes them.
When changes take effect
These times are the same for boards created, changed or deleted in the dashboard and by your server.
- A new board: your server can use its id at once. Clients can use its key within about 2.5 minutes; until then they get
Unknown leaderboard '<key>'.. - Changed settings: usually within 1 minute, at most about 2 minutes. Writes in between are scored with the previous settings.
- A deleted board: its id stops working within about 2 minutes. Until then, reads and writes by id can still succeed. Clients using its key get
Unknown leaderboard or cycle.once the id stops working, andUnknown leaderboard '<key>'.within about 2.5 minutes. Its records are erased within a day. See Deleting a board.
Scoring rules
The board's operator decides what a submission does. A submission never states its own rule: your server may name the operator in the request, but only as a cross-check that must match the board.
What each operator does
| Operator | First write in a cycle stores | A later write | accepted |
Subscore and metadata |
|---|---|---|---|---|
| Best | The submitted score | Replaces the stored score only when it is strictly better: higher on a "Higher is better" board, lower on a "Lower is better" board. An equal or worse score changes nothing. | true for the first write and for an improvement; false for an equal or worse score |
Replaced together with the score when the write is accepted; kept when it is not |
| Set | The submitted score | Replaces the stored score | Always true |
Replaced on every write; an omitted subscore becomes 0 and omitted metadata becomes empty (null) |
| Increment | The start score plus the submitted score: with the default start score of 0, the submitted score; with a start score of 1000, submitting 50 stores 1050 | Adds the submitted score to the stored one | Always true |
Replaced on every write, as for Set |
| Decrement ("Subtract") | The start score minus the submitted score: with a start score of 100, submitting 5 stores 95; with the default start score of 0, it stores -5 | Subtracts the submitted score from the stored one | Always true |
Replaced on every write, as for Set |
The score in every response is the score stored after the write. For a Best write that did not improve, it is the existing score. A Best write that is not accepted is still a valid write: it is not an error and it counts as a write for your plan.
A Decrement board counts down from its start score, which suits anything a user has a fixed amount of per cycle, such as lives or a budget. Give the board the amount as its start score, for example 3 lives or 1000 coins, and submit what each user loses or spends: with a start score of 100, two submissions of 5 leave 90. Every owner starts every cycle at the start score, and nothing carries over from the previous cycle. A user without a record in the current cycle has not submitted yet, so show them the start score. An Increment board adds to its start score the same way. To rank "fewest penalties first", an Increment board with "Lower is better" is usually clearer.
Negative values, bounds and totals
- Negative values are allowed. On an Increment board a negative value subtracts; on a Decrement board it adds.
- Size of one submission: the submitted score must be between -2^53 and 2^53 (±9,007,199,254,740,992). A value outside this range is refused.
- Start score: between -2^53 and 2^53, and not above the Max-score bound when the board has one, by the same rule as a submitted score. Only Increment and Decrement boards take one; on a Best or Set board it must be 0, and any other value is refused with
"A start score applies only to Increment and Decrement boards.". - Max-score bound: when the board has one, each submitted score must be less than or equal to it. It checks the value you send, not the stored result: on an Increment board it caps each increment, not the total, and it never limits negative values. A value above the bound is refused.
- Totals are not checked. Increment and Decrement totals can go past the Max-score bound, below zero and past 2^53. Keep totals within ±2^53 so that they rank exactly. A total beyond the 64-bit range makes the write fail.
Concurrent writes
Writes for the same owner on the same board are applied one at a time. Two simultaneous Increments of 5 add 10, two simultaneous Best submissions keep the better one, and two simultaneous first writes create one record. You do not need locking on your side. A write is final once it is answered; to undo it, remove the record (see Moderation and recovery).
Cycles
A board with a reset cadence starts a new cycle at every boundary. Boundaries are computed in UTC, so daylight-saving changes never shorten or lengthen a cycle: every daily cycle lasts 24 hours and every weekly cycle 7 days. Monthly and yearly cycles follow the calendar and differ in length.
Cycle ids
| Reset cadence | Cycle id format | Example at 2026-09-29 14:30:15 UTC | A new cycle starts |
|---|---|---|---|
| All-time | all |
all |
Never |
| Yearly | Year | 2026 |
1 January, 00:00 UTC |
| Monthly | Year-month | 2026-09 |
The 1st of the month, 00:00 UTC |
| Weekly | ISO week-numbering year, W, two-digit ISO week |
2026-W40 |
Monday, 00:00 UTC |
| Daily | Year-month-day | 2026-09-29 |
00:00 UTC |
| Hourly | Date, T, hour |
2026-09-29T14 |
Every full hour |
| Every minute | Date, T, hour and minute |
2026-09-29T14:30 |
Every full minute |
| Every second | Date, T, hour, minute and second |
2026-09-29T14:30:15 |
Every full second |
Weekly ids use the ISO week-numbering year, which can differ from the calendar year around New Year: 2024-12-30 belongs to 2025-W01 and 2021-01-01 to 2020-W53. Treat cycle ids as opaque text: take them from responses (cycleId in a submit or page response, or the cycle list) rather than computing them.
Which cycle a write lands in
Every write lands in the cycle that is current when it arrives. There is no way to write into a past or a future cycle. At a boundary the new cycle starts empty; nothing is carried over. A result submitted just after a boundary counts in the new cycle, even if the match was played before it.
Listing cycles
The cycle list contains the current cycle first, with isCurrent: true, even before it has a record. Then come the closed cycles that hold at least one record, newest first (sorted by id, descending). A closed cycle without records is not listed. A cycle of an earlier cadence that holds records stays in the list (the cadence can change only while the board holds no records, so this happens only to a record written in the minutes between a change and the moment it took effect).
Reading past cycles
Closed cycles stay readable; Five Minutes does not clean them up.
- Pages of a closed cycle (
GET .../records?cycleId=..., orClientGetLeaderboardRequestwithCycleId) come in rank order with positional ranks. - Single reads of a closed cycle (
GET .../records/{ownerId}?cycleId=..., orClientGetLeaderboardRecordRequestwithCycleId) return the record without a rank:rankisnullover REST andRankis-1on the wire. - The whole-board AroundPlayer view of a closed cycle reports
PlayerRank-1and serves the top of the board. UseWindow = Topwith anOffsetto page a past cycle. - An unknown cycle id (a typo, or a cycle that holds no records) is not an error for pages: a whole-board page is empty (
200, orSuccess = true) with a total of 0, and a view of a set of owners or of a group lists them all as unranked. Single reads answer404over REST andFound = falseon the wire, and a removal answers404.
Ranking
Order and ties
- On a "Higher is better" board, records are ordered by score, highest first. Records with the same score are ordered by owner id in reverse ordinal order:
user-branks aboveuser-a. - On a "Lower is better" board, records are ordered by score, lowest first. Records with the same score are ordered by owner id in ordinal order:
user-aranks aboveuser-b. - Ordinal order compares character codes. Owner ids made only of ASCII digits and letters of one case, such as hexadecimal unique user ids, are ordered this way in every read. Ties between ids that contain other characters, such as
-or_, or that mix letter case, can be ordered differently in the pages of a closed cycle. - The subscore plays no part in the order. To break ties by a second value, fold it into the score, for example
score = points * 1000000 + (999999 - seconds)on a "Higher is better" board, and keep the result within ±2^53.
Ranks are recomputed with every write and every removal, in every region (see Regions); there is nothing to recalculate.
When no rank is returned
Five Minutes keeps a live ranking of the current cycle of each board while the board is in use, separately in each region (see Regions). A read of the current cycle builds it in the region that serves the read: a page read (GET .../records without ownerIds, a whole-board ClientGetLeaderboardRequest, or the board's Standings tab in the dashboard), or a single read of an owner who has a record (GET .../records/{ownerId}, or ClientGetLeaderboardRecordRequest). A read builds it only when the cycle holds at least one record, so a new cycle stays cold until it is read after its first write. A region drops its ranking after 6 hours in which nobody reads the board there and no write, through any region, changes a score (a Best submission that is not accepted does not count as a change). While it is not built, the board is "cold" in that region, even when it is built in another:
- A submission is stored and scored as usual, but its response carries no rank:
rankisnullover REST andRankis-1on the wire. A write never builds the ranking. - The first read builds the ranking and returns ranks. On a large board this first read takes longer than later ones.
A rank can also be missing, for a few seconds, during a temporary disruption; the record is stored all the same. Page reads always carry ranks. Treat the rank as optional. When you need it right after a write, read the owner's record: that read builds the ranking and returns the rank.
Ranking a subset
Your server can rank a chosen set of owners among themselves by passing ownerIds to GET .../records, for example a clan's members:
rankis the position within the set, starting at 0.totalCountis the number of owners in the set that have a record in the cycle;rosterCountis the number of distinct ids you sent. Duplicates are ignored.- On the last page,
unrankedOwnerIdslists the ids that have no record in the cycle. On other pages it is empty. offsetandlimitpage the ranked set. It works on the current cycle and on closed ones, and it never builds the live ranking.- The ids travel in the URL, which must stay under about 8 KB: up to 100 ids of 64 characters fit.
There is no friends, group or around-me parameter over REST. Unknown parameters such as friendsOf are ignored, so the call silently returns the whole board. Your server passes the roster explicitly. Clients can narrow a view to one of their user's own personal groups; see Read a leaderboard view (ClientGetLeaderboardRequest).
Regions
Five Minutes serves the application from several regions (see Host). Leaderboard records are shared by all of them, and every region ranks the current cycle of a board from the writes made through all regions. Your server and your clients can use any board whichever region serves them.
- Records are the same everywhere. A write served by any region, from your server or from a client, is stored once for the application. Single-record reads return the current score, subscore and metadata in every region, and so does every entry of a page. Closed cycles are read from the stored records, so their pages and ranks are the same in every region.
- Every region follows every write. A region builds its live ranking of the current cycle from the stored records the first time the board is read there (see When no rank is returned). From then on it takes in every write and every removal, whichever region processed it: a write through the same region at once, a write through another region normally within 1 to 2 seconds, in the order the writes were stored. While the region replaces a failed server instance, writes through other regions can take up to about 15 seconds longer to arrive; none are lost.
- Until a write through another region has arrived, this region's pages, ranks and totals are those from before it: a new owner is not yet listed or counted, an owner whose score changed keeps the position of the old score while the entry already shows the new score, and a removed owner can still be listed. The
rankin the answer to a write comes from the region that processed it and already includes that write. - Built per region. Whether a board's ranking is built, and the 6-hour idle drop, are separate in each region: a write can answer with a rank in one region and without one (
nullor -1) in another where the board has not been read. Rarely, a region drops all of its rankings after a disruption and builds each one again on the board's next read; until then, writes through that region answer without a rank. - Client submission limits are counted per region: a user connected to two regions at once gets the limit in each.
Server API reference
Calling the Server API
Your server calls https://signalr.fiveminutes.cloud/api/v1/... with Authorization: Bearer <server token>. Get the token with POST /api/v1/api-clients/authenticate and the body {"applicationId":"<application id>","clientSecret":"<API key secret>"}; it is valid for expiresInSeconds (600) seconds and there is no refresh token. A missing, invalid or expired token gets 401 with an empty body; a token without the needed scope gets 403 with an empty body. Error bodies, rate limits and idempotency rules: Five Minute Server API.
Leaderboard routes need these scopes:
Application.Scores.Read("Leaderboards: read"): list records, read one owner's record, list cycles.Application.Scores.Write("Leaderboards: write"): submit a score, remove a record.
Write does not imply Read: a server that submits scores and then reads ranks needs both. Application.Admin.FullAccess ("Full access") grants both. The routes that create, read, change and delete boards (/api/v1/leaderboards and /api/v1/leaderboards/{leaderboardId}) are documented in Leaderboard definitions: reading needs Application.Scores.Read, the writes need Application.Leaderboards.Write ("Leaderboards: manage boards"), which Application.Scores.Write does not include. See Scopes and authorization.
Every route addresses the board by its GUID id, and acts on the board of the application the token belongs to. A board key in the path, or the id of another application's board, answers 404 with an empty body. The general error model (Errors) applies to all routes; the tables below list what is specific to each one. The following answers can come from any of the five routes:
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 401 | empty | The token is missing, invalid or expired. | Exchange a new token once, then stop. |
| 403 | empty | The token lacks the route's scope. | No. Add the scope to the API key. |
| 404 | empty | No board with this id in the token's application. | No. Check the id. |
| 429 or 503 | empty or HTML | The edge rate limit rejected the request. | Yes, with backoff. |
| 502 | application/problem+json, detail "The leaderboard service could not serve the request." |
Five Minutes could not complete the request. | Yes, with backoff. |
| 503 | application/problem+json, detail "The service is temporarily unavailable. Please retry shortly.", header Retry-After: 5 |
A temporary outage; the request had no effect. | Yes, after Retry-After. |
| 500 | {"error": "An unexpected error occurred."} |
An unexpected failure. | GET and DELETE: yes. POST: only with an Idempotency-Key. |
List records (GET /api/v1/leaderboards/{leaderboardId}/records)
Pages a board's records in rank order, for the whole board or for a set of owners.
| Method | Path | Scope | Success | Idempotency-Key |
|---|---|---|---|---|
| GET | /api/v1/leaderboards/{leaderboardId}/records |
Application.Scores.Read |
200 | No |
Parameters
| Name | In | Type | Required | Default | Rules |
|---|---|---|---|---|---|
leaderboardId |
path | GUID | Yes | none | The board id, not the key. |
cycleId |
query | string | No | The current cycle | A cycle id from the cycle list or from an earlier response. |
offset |
query | integer | No | 0 | Zero-based position of the first record. A negative value counts as 0. |
limit |
query | integer | No | 50 | Number of records. Values outside 1-100 are clamped to 1-100. |
ownerIds |
query | string, repeated | No | none | Ranks only these owners among themselves. Repeat the parameter for each id: ?ownerIds=a&ownerIds=b. |
Example request
GET /api/v1/leaderboards/<leaderboard id>/records?offset=0&limit=3 HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Example response
{
"entries": [
{
"ownerId": "<unique user id>",
"score": 18250,
"subscore": 412,
"rank": 0,
"metadataJson": "{\"name\":\"Ada\",\"character\":\"mage\"}"
},
{
"ownerId": "user-42",
"score": 17900,
"subscore": 388,
"rank": 1,
"metadataJson": null
},
{
"ownerId": "user-17",
"score": 17900,
"subscore": 450,
"rank": 2,
"metadataJson": "{\"name\":\"Bo\"}"
}
],
"totalCount": 5812,
"cycleId": "2026-W40",
"unrankedOwnerIds": [],
"rosterCount": 0
}
user-42 ranks above user-17 with the same score because ties are ordered by owner id (reverse ordinal on this "Higher is better" board); the subscore plays no part.
A read of a set of owners, one of whom has no record in the cycle:
GET /api/v1/leaderboards/<leaderboard id>/records?ownerIds=user-17&ownerIds=user-42&ownerIds=user-77 HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
{
"entries": [
{
"ownerId": "user-42",
"score": 17900,
"subscore": 388,
"rank": 0,
"metadataJson": null
},
{
"ownerId": "user-17",
"score": 17900,
"subscore": 450,
"rank": 1,
"metadataJson": "{\"name\":\"Bo\"}"
}
],
"totalCount": 2,
"cycleId": "2026-W40",
"unrankedOwnerIds": [
"user-77"
],
"rosterCount": 3
}
Responses
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 200 | JSON page | The page. entries is empty past the end of the board and for a cycle id that holds no records. |
Not applicable. |
| 400 | empty | A query value has the wrong format, for example limit=abc. |
No. Fix the request. |
| 404 | empty | No board with this id. | No. |
Notes
- Without
ownerIds,rankis the record's position on the whole board (offset+ its index inentries) andtotalCountis the number of records in the cycle.unrankedOwnerIdsis[]androsterCountis 0. - With
ownerIds, ranks,totalCount,rosterCountandunrankedOwnerIdsare as described in Ranking a subset. rankis nevernullin a page.metadataJsonis a string (JSON text inside a JSON string) ornull.- A read of the current cycle without
ownerIdsbuilds the board's live ranking (see When no rank is returned). - Pages are read one at a time. When scores change between two page reads, a record can move from one page to another, so it can appear twice or not at all.
- Parameters such as
friendsOf,groupIdoraroundOwnerIddo not exist; they are ignored and the whole board is returned.
Read one owner's record (GET /api/v1/leaderboards/{leaderboardId}/records/{ownerId})
Reads one owner's record and, for the current cycle, its rank on the whole board.
| Method | Path | Scope | Success | Idempotency-Key |
|---|---|---|---|---|
| GET | /api/v1/leaderboards/{leaderboardId}/records/{ownerId} |
Application.Scores.Read |
200 | No |
Parameters
| Name | In | Type | Required | Default | Rules |
|---|---|---|---|---|---|
leaderboardId |
path | GUID | Yes | none | The board id, not the key. |
ownerId |
path | string | Yes | none | 1-128 characters. URL-encode it. An id containing / cannot be addressed. |
cycleId |
query | string | No | The current cycle | A cycle id from the cycle list or from an earlier response. |
Example request
GET /api/v1/leaderboards/<leaderboard id>/records/<unique user id> HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Example response
{
"ownerId": "<unique user id>",
"score": 18250,
"subscore": 412,
"rank": 0,
"metadataJson": "{\"name\":\"Ada\",\"character\":\"mage\"}"
}
Responses
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 200 | JSON record | The owner's record. | Not applicable. |
| 404 | empty | No board with this id, or the owner has no record in the cycle (also for a cycle id that does not exist). | No. |
Notes
rankis the zero-based position on the whole board in the current cycle. It isnullfor a closed cycle, and can benullfor a few seconds during a temporary disruption.- For the current cycle, this read builds the board's live ranking, so the rank is present even when the board was cold.
- A
404does not tell an unknown board from a missing record. To check that the board exists, list its cycles.
List cycles (GET /api/v1/leaderboards/{leaderboardId}/cycles)
Lists the board's current cycle and every closed cycle that holds records.
| Method | Path | Scope | Success | Idempotency-Key |
|---|---|---|---|---|
| GET | /api/v1/leaderboards/{leaderboardId}/cycles |
Application.Scores.Read |
200 | No |
Parameters
| Name | In | Type | Required | Default | Rules |
|---|---|---|---|---|---|
leaderboardId |
path | GUID | Yes | none | The board id, not the key. |
Example request
GET /api/v1/leaderboards/<leaderboard id>/cycles HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Example response
{
"cycles": [
{
"cycleId": "2026-W40",
"isCurrent": true
},
{
"cycleId": "2026-W39",
"isCurrent": false
},
{
"cycleId": "2026-W37",
"isCurrent": false
}
]
}
Here week 38 is missing because nobody submitted a score that week.
Responses
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 200 | JSON list | The cycles. The first entry is always the current cycle. | Not applicable. |
| 404 | empty | No board with this id. | No. |
Notes
- The order and contents follow Listing cycles: the current cycle first, then closed cycles with records, newest first. An all-time board lists
allonly, unless a cycle of an earlier cadence holds records. - Use it to check that a board id exists: it answers
404only for an unknown board.
Submit a score (POST /api/v1/leaderboards/{leaderboardId}/records)
Submits a score for one owner as your server. The board's operator decides what the write does.
| Method | Path | Scope | Success | Idempotency-Key |
|---|---|---|---|---|
| POST | /api/v1/leaderboards/{leaderboardId}/records |
Application.Scores.Write |
200 | Yes (header) |
Parameters
| Name | In | Type | Required | Default | Rules |
|---|---|---|---|---|---|
leaderboardId |
path | GUID | Yes | none | The board id, not the key. |
Idempotency-Key |
header | string | No, but recommended | none | 1-128 characters, not only whitespace. Build it from your own ids, for example match-8812:<ownerId>. |
ownerId |
body | string | Yes | none | 1-128 characters, not only whitespace. For a user's record, the unique user id. |
score |
body | integer (64-bit) | No; always send it | 0 | Between -2^53 and 2^53, and at most the board's Max-score bound. On Increment and Decrement boards, the amount to add or subtract. |
subscore |
body | integer (64-bit) | No | 0 | Display only. |
operator |
body | string | No | none | "Best", "Set", "Increment" or "Decrement". When present it must equal the board's operator. |
metadataJson |
body | string or null | No | null |
Text stored as-is. Send JSON as a string, at most 4096 characters recommended. |
Example request
POST /api/v1/leaderboards/<leaderboard id>/records HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Content-Type: application/json
Idempotency-Key: match-8812:<unique user id>
{
"ownerId": "<unique user id>",
"score": 18250,
"subscore": 412,
"operator": "Best",
"metadataJson": "{\"name\":\"Ada\",\"character\":\"mage\"}"
}
Example response
{
"accepted": true,
"score": 18250,
"rank": 0,
"cycleId": "2026-W40"
}
Responses
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 200 | JSON result | The write was processed. accepted is false only for a Best submission that did not improve the record. |
Not applicable. |
200 with Idempotency-Replayed: true |
JSON result | A retry with a known key, answered by the region that answered the first request: its stored response. The write was not applied again. | Not applicable. |
200 without Idempotency-Replayed |
JSON result | A retry with a known key that the answering region has no stored response for, for example one served by another region or one after a 5xx: accepted, score and cycleId of the first write and the owner's current rank. The write was not applied again. |
Not applicable. |
| 400 | JSON string "OwnerId is required." |
ownerId is missing, empty or only whitespace. |
No. |
| 400 | JSON string "OwnerId is required and is at most 128 characters." |
ownerId is longer than 128 characters. |
No. |
| 400 | JSON string "Unknown operator '<value>'." |
operator was sent as a number that is not an operator. |
No. |
| 400 | JSON string "The stated operator disagrees with the board definition; the definition decides the write semantics." |
operator differs from the board's operator. |
No. Leave operator out or correct it. |
| 400 | JSON string "Score exceeds the board's MaxScore bound or the representable range." |
score is above the board's Max-score bound, or outside ±2^53. |
No. |
| 400 | JSON string "The idempotency key must be 1-128 characters." |
The Idempotency-Key header is longer than 128 characters or only whitespace. |
No. |
| 400 | empty | The body is not valid JSON, or a property has the wrong type: metadataJson sent as an object, operator an unknown name, score not a whole number. |
No. |
| 409 | JSON string "A request with this idempotency key is still being processed; retry shortly.", header Retry-After: 1 |
The first request with this key, served by the same region, has not finished. | Yes, after Retry-After, with the same key and body. |
| 415 | empty | The request has no Content-Type: application/json. |
No. |
| 422 | JSON string "This idempotency key was already used for a different request." |
The key was used in the last 24 hours, in any region, with another body or another board. | No. |
Notes
- Result:
acceptedisfalseonly on a Best board when the score is not strictly better than the stored one.scoreis the stored score after the write.cycleIdis the cycle the write landed in.rankis the owner's zero-based position after the write, ornullwhile the board's ranking is cold (see When no rank is returned). - Operator: leave
operatorout unless you want the cross-check. The board's operator always decides. - Subscore and metadata: on Set, Increment and Decrement boards every write replaces them, so an omitted
subscorestores 0 and an omittedmetadataJsonstoresnull. On a Best board they change only when the write is accepted. - Client submission: this route is your server's write. It works whether or not the board allows client submission and is not subject to the per-user submission limit.
- Idempotency-Key: 1-128 characters. A write with a key is applied at most once: for 24 hours after it was applied, a request with the same key and the same request, served by any region, gets that write's result and is neither applied nor counted again.
- In the region that answered the first request, once that answer was stored, a retry gets the stored response with
Idempotency-Replayed: true, including therankas it was at the first write. - Any other retry, for example one served by another region or one after a
5xx, gets200without that header:accepted,scoreandcycleIdof the first write, and the owner'srankas it is now (nullwhile the board's ranking is cold in that region). A Best write that was accepted staysaccepted: trueon every retry. - When the first request was not applied, for example because it failed before the score was saved, the retry applies it.
- While the first request is still running, a retry served by the same region gets
409. Two requests with the same key that run at the same time in different regions are applied once: the one that finds the key taken waits for the other and then gets its result. 400and404answers are not stored: once you fix the request, a retry with the same key runs.- A key belongs to this route and your application, not to one board: reusing it with another board id or another body gets
422in every region. All API keys of the application share the key space. - After 24 hours the key is forgotten, and a request that carries it is a new write.
- In the region that answered the first request, once that answer was stored, a retry gets the stored response with
- Without a key every request is a new write, so a retry after a timeout or a
5xxcan apply an Increment or Decrement score twice. - Plan: every processed write counts as one leaderboard write for your plan, whether or not
acceptedistrue(see Plan allowances).
Remove a record (DELETE /api/v1/leaderboards/{leaderboardId}/records/{ownerId})
Deletes one owner's record from one cycle of a board. The remaining records re-rank at once in the region that processed the removal, and within 1 to 2 seconds in the others.
| Method | Path | Scope | Success | Idempotency-Key |
|---|---|---|---|---|
| DELETE | /api/v1/leaderboards/{leaderboardId}/records/{ownerId} |
Application.Scores.Write |
204 | No |
Parameters
| Name | In | Type | Required | Default | Rules |
|---|---|---|---|---|---|
leaderboardId |
path | GUID | Yes | none | The board id, not the key. |
ownerId |
path | string | Yes | none | 1-128 characters. URL-encode it. An id containing / cannot be addressed. |
cycleId |
query | string | No | The current cycle | Any cycle id, closed cycles included. |
Example request
DELETE /api/v1/leaderboards/<leaderboard id>/records/user-42?cycleId=2026-W39 HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Example response
HTTP/1.1 204 No Content
Responses
| Status | Body shape | Meaning | Retry? |
|---|---|---|---|
| 204 | empty | The record was deleted. | Not applicable. |
| 404 | empty | No board with this id, or no record for this owner in the cycle. | No. A repeat after a success also answers 404. |
Notes
- Works on any cycle, closed ones included. Without
cycleIdit removes the record from the current cycle. - It removes one record. The owner can submit again at once; to stop that, also ban the user (see Chat, users and moderation endpoints).
- There is no idempotency key; repeating the call is harmless.
- A removal does not count as a leaderboard write.
Posting many scores
There is no batch endpoint: every score is one POST. When your server posts many scores at once, for example the results of a tournament or a data migration:
- Pace the calls at 5 requests per second or less per public IP address and region. The edge limit counts every request to the host, and all servers behind one IP address share it. See Rate limits and quotas.
- Give every write an Idempotency-Key built from your own ids:
<match id>:<owner id>, for examplematch-8812:user-42. When one result is posted to several boards, include the board key:match-8812:weekly_arena:user-42. If the key would be longer than 128 characters, use a hash of it, such as 64 hexadecimal characters of SHA-256. - Retry
409(afterRetry-After),429,500,502,503, timeouts and dropped connections with exponential backoff and jitter (for example 0.5 s, 1 s, 2 s, 4 s, each plus up to 50 % random), with the same key and the same body. Give up after a bounded number of attempts and log the write for later; a retry with the same key within 24 hours of the first attempt is still applied at most once. - Do not retry
400,403,404or422: fix the request or the configuration first.
Exactly-once totals
Five Minutes applies a keyed write at most once, which makes Increment and Decrement totals exact as long as every retry repeats the key:
- Your server: a
POST .../recordswith anIdempotency-Keyis applied at most once for 24 hours after it was applied, in every region, also when the first attempt ended in a timeout or a5xx. Retry it with the same key and body until you get an answer, and the score counts exactly once. The answer to a retry tells you the outcome: it carries the first write'saccepted,scoreandcycleId. - Clients: the MessageId of a
ClientSubmitScoreRequestis its key, for 24 hours and in every region. A resend of the same message, with the same MessageId, is never applied twice (see After a lost response).
A score can still count twice when a write is repeated without its key: a retry from your server without an Idempotency-Key, a retry more than 24 hours after the first attempt, or a client resend with a new MessageId.
When your server cannot promise to finish its retries within 24 hours, for example when failed writes wait in a queue for a manual replay, keep the total in your own database and write it to a board whose operator is Set, with an Idempotency-Key built from the board, the owner and the total's version, such as season_points:<owner id>:<version>. A repeated Set writes the same value, so a retry is safe at any time.
Recipes
Recipes for your server
Find a board's id by its key
- At start-up your server calls
GET /api/v1/leaderboardswith a token holdingApplication.Scores.Read. Success is200with every board of the application. - For each key your app uses, it takes the board whose
keymatches exactly, with the same letter case, and keeps itsidfor the record routes. - A key that is missing from the list has no board yet: create it in the dashboard, or with
POST /api/v1/leaderboards(Application.Leaderboards.Write), then list again.
Post a match result safely
- Your server gets a server token whose API key holds
Application.Scores.Write(addApplication.Scores.Readif you also read ranks), and caches it until shortly beforeexpiresInSecondsruns out. - Your server sends, for each user in the match,
POST /api/v1/leaderboards/{leaderboardId}/recordswithownerIdset to the unique user id andIdempotency-Key: <match id>:<unique user id>. Success is200. - Your server reads
accepted,scoreandrankfrom the answer. Whenrankisnull, it reads the record withGET .../records/{ownerId}if it needs the rank. - On
409,429,500,502,503or a network error, your server waits and repeats step 2 with the same key and body. When the first attempt was applied, the repeat gets its result and is not applied again. - On
400,403,404or422, your server stops and logs the answer.
Show "you are here" from your server
- Your server calls
GET /api/v1/leaderboards/{leaderboardId}/records/{ownerId}.200gives the user'srankr;404means the user has no record, so show the top of the board instead (GET .../records?limit=10). - Your server computes
offset = max(0, r - limit / 2)with integer division, for example r = 1204 and limit = 10 gives offset 1199. - Your server calls
GET .../records?offset=1199&limit=10. The user's record is inentries, andtotalCountgives "1,205th of 58,310" (rank + 1 of totalCount). - If
rankin step 1 isnull(a brief disruption), repeat step 1 after a second.
Rank a clan's members
- Your server looks up the unique user ids of the clan's members in its own data: Five Minutes keeps no server-side groups for this.
- Your server calls
GET .../records?ownerIds=<id 1>&ownerIds=<id 2>&...with up to 100 ids. - Your server shows
entrieswith their relative ranks, then the ids inunrankedOwnerIdsas "no score".totalCountis the number of members with a score androsterCountthe number of members asked for.
Run a clan-versus-clan board
- You create a board in the dashboard, for example key
clan_points, operator Increment, "Higher is better", Weekly, Allow client submission off. - Your server adds points for a clan with
POST .../records,ownerId<clan id>(an id you choose for the clan that nobody can guess; see Concepts),score= the points earned andmetadataJson= what clients should show, such as"{\"clan\":\"Ember Wolves\"}", with anIdempotency-Keyper match. Send the metadata with every write: on an Increment board every write replaces it. - Clients read the board with
ClientGetLeaderboardRequestandWindow = Top, and show each clan from the entry'sMetadataonly.OwnerId,OwnerDisplayIdandOwnerNameare empty because a clan is not a user; your app does not rely on that, because a user who logged in with the clan's id would fill the display id and name.PlayerRankis -1 because the user has no record on this board.
Only your server can write a clan's record; a client can write only its own user's record.
Announce last cycle's winners
- Your server calls
GET .../cyclesand takes the first entry withisCurrent: false: the most recent closed cycle that holds records. Check that it is the cycle you expect; a cycle without records is not listed. - Your server calls
GET .../records?cycleId=<that id>&limit=3. The entries are the winners with ranks 0, 1 and 2. - Ranks are unique even for equal scores (ties are ordered by owner id). If equal scores should share a prize, compare
scorevalues yourself. - Your server stores the payout under its own id before paying, so a repeated run does not pay twice.
Remove a cheater from every board and cycle
- Your server keeps the list of its board ids; there is no API that lists boards.
- For each board, your server calls
GET .../cycles. - For each cycle id, your server calls
DELETE .../records/{ownerId}?cycleId=<cycle id>.204means removed;404means the user had no record there. - Your server paces the calls at 5 per second or less, and bans the user if they should not submit again.
In the dashboard, removal works on selected owners of one board, in its current cycle only; see Moderation and recovery.
Recipes for the client
Show a user's best
- The client sends
ClientGetLeaderboardRecordRequestwithLeaderboardKey, andCycleIdempty for the current cycle. - Five Minutes answers
ServerGetLeaderboardRecordResponse. WithSuccess = trueandFound = true, showEntry.Score, andEntry.Rank + 1whenEntry.Rankis 0 or more (it is -1 for a closed cycle or during a brief disruption); withFound = false, show "no score".
With the npm packages: const mine = await client.getLeaderboardRecord("weekly_arena");, then read mine.Found and mine.Entry.
Show the board around the user
- The client sends
ClientGetLeaderboardRequestwithLeaderboardKey,Window = AroundPlayerandLimit = 10. - Five Minutes answers
ServerGetLeaderboardResponsewith up to 10 entries around the user.PlayerRank + 1ofTotalCountgives "1,205th of 58,310". A user without a record gets the top of the board andPlayerRank-1.
With the npm packages: await client.getLeaderboard("weekly_arena", { Limit: 10 }); the window is AroundPlayer unless you pass another.
Show the user's friends
- The client keeps the user's friends in a personal group, for example
friends, withClientAddUsersToPersonalGroupRequest. - The client sends
ClientGetLeaderboardRequestwithGroupMoniker = "friends"andWindow = AroundPlayer. - Five Minutes answers with the friends and the user ranked among themselves; friends without a score come last with
Rank-1. Each row names the friend byOwnerDisplayIdandOwnerName; the user's own row hasIsSelftrue.
With the npm packages: await client.request("ClientAddUsersToPersonalGroupRequest", { GroupMoniker: "friends", OtherUserIds: [friendDisplayId] }) once, then await client.getLeaderboard("weekly_arena", { GroupMoniker: "friends" }).
Client messages
Client prerequisites
These messages need protocol 2.0 and a completed login: send them only after ServerWelcome. Anything sent earlier makes the server close the connection. Give every message a new MessageId. Each request gets a ServerAck as soon as the server has received it, when IsAckRequested is true, then one typed response whose ResponseToMessageId is your MessageId. The ServerAck says only that the request arrived; the typed response says whether it succeeded. Failures arrive in the typed response as Success=false with a FailureReason. The server never sends these responses to a connection below protocol 2.0.
See Message conventions in the wire protocol guide. The tables below give each message's fields and what they mean; the client library sends and reads the messages.
With the npm packages 2.0, submitScore(), getLeaderboard() and getLeaderboardRecord() send these messages; their options are the fields of the tables. They resolve with the typed response when Success is true and reject with a RequestFailedError, whose message is the FailureReason, when it is false. The .NET library and the npm packages refuse to send a message that breaks a length or range rule of the tables, such as Limit outside 1-100 or Metadata over 4096 characters: the npm call rejects at once with a MessageValidationError whose message is the FailureReason the tables list, and nothing is sent (see Checks before sending).
What a client can do:
- Write only its own user's record, and only on boards with Allow client submission on. The owner of the record is always the connected user's unique user id; there is no owner field.
- Read every board of the application by key, whether or not the board allows client submission.
- See other users only by display id and name. An entry's
OwnerIdis filled only on the user's own record, which also hasIsSelftrue; see One record on the wire (LeaderboardEntry).
Every message, reads included, also counts toward the connection's general message rate limit described under Pacing in the wire protocol guide.
Submit a score (ClientSubmitScoreRequest)
Client -> server, any time after ServerWelcome. Reply: ServerAck, then ServerSubmitScoreResponse.
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | 1-128 characters, exact and case-sensitive | The board's key. |
Score |
whole number | Between -2^53 and 2^53, and at most the board's Max-score bound | The score, or on Increment and Decrement boards the amount to add or subtract. |
Subscore |
whole number | Any value; 0 when not used | Display value stored with the record. Never used for ordering. |
Metadata |
text | At most 4096 characters; empty for none | Text stored with the record and returned to readers, for example JSON with a display name. |
MessageId |
GUID | New for every new submission; the same when you resend one | Echoed in ResponseToMessageId. Also the submission's idempotency key. |
IsAckRequested |
true/false | true asks for a ServerAck, which comes as soon as the server has the request, before the typed response. |
Rules:
- The record belongs to the connected user: its owner id is the unique user id (
UniqueUserId). A unique user id longer than 128 characters, which only a legacy id can be, cannot hold a record. - The board must have Allow client submission on.
- The scoring rules of the board apply as for your server's writes. On Set, Increment and Decrement boards, every submission replaces the subscore and metadata; an empty
Metadatastores none. - Rate limit: each user can submit at most the plan's "Messages Per Second Per User" × 10 scores per 10-second window. Windows are aligned to the UTC clock: they start at :00, :10, :20, :30, :40 and :50 seconds. On the Free and Subscription plans the limit is 10 per window; a plan with a higher "Messages Per Second Per User" allows proportionally more (100 allows 1,000 per window). The limit is per user and application, shared by all boards and by all of the user's connections to one region. Every submission that reaches the check counts, including the ones it refuses and resends of an earlier message, so a client that keeps sending while refused stays refused until the next window. Submissions refused before the check (unknown board, board closed to clients, invalid score) do not count.
- The MessageId is the idempotency key. For 24 hours, in every region, a submission with the same MessageId from the same user is applied at most once: a resend gets the result of the first submission. See After a lost response.
ServerSubmitScoreResponse fields
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | The key you sent. | |
Accepted |
true/false | false on a Best board when the score was not strictly better, and when Success is false; otherwise true (always on Set, Increment and Decrement boards). |
|
Score |
whole number | The score stored after the write. | |
Rank |
whole number | -1 when the ranking is cold or briefly unavailable | The user's zero-based rank on the whole board after the write. See When no rank is returned. |
CycleId |
text | Empty on failure | The cycle the write landed in, for example 2026-W40. |
ResponseToMessageId |
GUID | Your MessageId. |
|
Success |
true/false | false when the submission was refused. |
|
FailureReason |
text | Empty on success | Why it was refused. |
The response to a resend of a submission that was already applied carries Accepted, Score and CycleId of that first submission, and the user's Rank as it is now.
Example
using FiveMinutes.Model.Messages.Client;
using FiveMinutes.Model.Messages.Server;
var submit = new ClientSubmitScoreRequest
{
LeaderboardKey = "weekly_arena",
Score = 18250,
Subscore = 412,
Metadata = "{\"name\":\"Ada\",\"character\":\"mage\"}"
};
Guid pendingSubmit = submit.MessageId; // a new GUID for every message
connection.Send( submit ); // sent through a client library
void OnServerSubmitScoreResponse( ServerSubmitScoreResponse response )
{
if( response.ResponseToMessageId != pendingSubmit )
return;
if( !response.Success )
{
ShowError( response.FailureReason );
return;
}
string place = response.Rank >= 0 ? $"#{response.Rank + 1}" : "ranked shortly";
ShowResult( response.Accepted ? "New best!" : "No new best", response.Score, place );
}
Example with the npm packages
import { RequestFailedError, RequestTimeoutError } from "@fiveminutes-io/chat-client";
import { newMessageId } from "@fiveminutes-io/contracts";
const messageId = newMessageId(); // the submission's key: keep it until the outcome is known
try {
const response = await client.submitScore("weekly_arena", 18250, {
Subscore: 412,
Metadata: JSON.stringify({ name: "Ada", character: "mage" }),
MessageId: messageId,
});
const place = response.Rank >= 0 ? `#${response.Rank + 1}` : "ranked shortly";
showResult(response.Accepted ? "New best!" : "No new best", response.Score, place);
} catch (error) {
if (error instanceof RequestTimeoutError) {
// Unknown outcome: resend unchanged with { MessageId: messageId } (see After a lost response).
// error.acknowledged: the server has the submission, so wait 5 minutes after the last send first.
} else if (error instanceof RequestFailedError) {
showError(error.failureReason);
}
}
Failure reasons of a submission
| FailureReason | Cause | Client action |
|---|---|---|
Parameter 'LeaderboardKey' must be 1-128 characters |
The key is empty, only whitespace or too long. | Fix the key. |
Parameter 'Metadata' cannot exceed 4096 characters |
Metadata is too long. |
Shorten it. |
Unknown leaderboard '<key>'. |
No board has this key: a typo, wrong letter case, a board created less than about 2.5 minutes ago, or a deleted board. | Check the key. For a new board, try again a few minutes later. |
Client submission is not allowed on this leaderboard. |
Allow client submission is off. | Submit through your server, or turn the setting on. |
Invalid submission. |
The score is above the Max-score bound or outside ±2^53, or the unique user id is longer than 128 characters. | Do not resend the same score. |
Too many submissions. Try again shortly. |
The user's submission limit for the current 10-second window is used up. | Wait for the next 10-second window. |
Unknown leaderboard or cycle. |
The board was deleted moments ago. | Stop using the key. |
This message was already used for a different submission. |
The MessageId was used by this user in the last 24 hours for a submission with other content: another board, score, subscore or metadata. | Give every new submission a new MessageId; resend only unchanged messages. |
Leaderboards are temporarily unavailable. |
A temporary failure. The submission may or may not have been applied. | Send the same message again, unchanged and with the same MessageId, once 5 minutes have passed since you sent it (see After a lost response). |
Read a leaderboard view (ClientGetLeaderboardRequest)
Client -> server, any time after ServerWelcome. Reply: ServerAck, then ServerGetLeaderboardResponse.
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | 1-128 characters, exact and case-sensitive | The board's key. |
CycleId |
text | Empty for the current cycle | The cycle to read, as returned in earlier responses. |
GroupMoniker |
text | At most 128 characters; empty for the whole board | The name of one of the user's own personal groups. |
Window |
LeaderboardWindow |
AroundPlayer or Top |
Which part of the board to return. |
Offset |
whole number | 0 or more; used only with Top |
Zero-based position of the first record. |
Limit |
whole number | 1-100 (the contracts library defaults to 50) | Number of records. |
MessageId |
GUID | New for every message | Echoed in ResponseToMessageId. |
IsAckRequested |
true/false | true asks for a ServerAck, which comes as soon as the server has the request, before the typed response. |
Whole board (GroupMoniker empty):
Window = AroundPlayer: Five Minutes looks up the user's rank r on the board and starts the page atmax(0, r - Limit / 2)(integer division). When the user has no record, r is -1 and the page starts at the top. Near the end of the board the page is not shifted up, so it can hold fewer thanLimitentries.Window = Top: the page starts atOffset.- A closed cycle has no live ranks:
AroundPlayerreportsPlayerRank-1 and serves the top. UseTopwithOffsetto page a closed cycle. - An unknown
CycleIdgives an empty page withSuccess = trueandTotalCount0. - A whole-board read of the current cycle builds the board's live ranking.
Group view (GroupMoniker set):
- The group is the connected user's own personal group with that name, created with
ClientAddUsersToPersonalGroupRequestor with your server's personal group endpoints. A client cannot view another user's group. - The view ranks the group's members plus the user among themselves. At most 100 users, including the user, can be ranked; a larger group is refused with
Group is too large to rank (at most 100 members)., never cut short. - Ranks,
PlayerRankandOffsetare positions within the group, starting at 0.TotalCountis the number of group members, the user included, who have a record. AroundPlayercentres the page on the user within the group;Topstarts atOffset. Group views work on closed cycles too, and therePlayerRankis the user's position in the group.- Members without a record come after the ranked entries on the last page, with
Rank-1,Score0,Subscore0 and emptyMetadata. Like every other member they carry theirOwnerDisplayIdandOwnerNameand noOwnerId. They are not counted inTotalCount, so the last page can hold more thanLimitentries. - A name that matches no group of the user, or a group without members, is not an error: the view contains only the user.
Unknown group '<name>'.appears only when the group lookup itself fails.
ServerGetLeaderboardResponse fields
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | The key you sent. | |
CycleId |
text | The cycle served; the current cycle's id when you sent an empty CycleId. |
|
GroupMoniker |
text | The group name you sent; empty for the whole board. | |
Window |
LeaderboardWindow |
The window you sent. | |
Offset |
whole number | Position within the scope of the first entry. | |
PlayerRank |
whole number | -1 when unknown | The user's zero-based rank within the scope; -1 when the user has no record there, and in a whole-board view of a closed cycle. |
TotalCount |
whole number | The number of ranked records in the scope. Only this response carries a total. | |
Entries |
list of LeaderboardEntry |
The records of the page, best first. | |
ResponseToMessageId |
GUID | Your MessageId. |
|
Success |
true/false | false when the read was refused. |
|
FailureReason |
text | Empty on success | Why it was refused. |
Example
using FiveMinutes.Model.Enums;
var aroundMe = new ClientGetLeaderboardRequest
{
LeaderboardKey = "weekly_arena",
Window = LeaderboardWindow.AroundPlayer,
Limit = 10
};
var friends = new ClientGetLeaderboardRequest
{
LeaderboardKey = "weekly_arena",
GroupMoniker = "friends",
Window = LeaderboardWindow.Top,
Offset = 0,
Limit = 20
};
connection.Send( aroundMe );
connection.Send( friends );
void OnServerGetLeaderboardResponse( ServerGetLeaderboardResponse response )
{
if( !response.Success )
{
ShowError( response.FailureReason );
return;
}
bool clanBoard = response.LeaderboardKey == "clan_points";
foreach( var entry in response.Entries )
{
string place = entry.Rank >= 0 ? $"#{entry.Rank + 1}" : "-";
if( clanBoard )
{
AddRow( place, NameFromMetadata( entry.Metadata ), entry.Score, "" );
continue;
}
string owner = entry.IsSelf ? "You"
: entry.OwnerDisplayId != "" ? entry.OwnerName
: "Unknown user";
AddRow( place, owner, entry.Score, entry.OwnerDisplayId );
}
if( response.PlayerRank >= 0 )
ShowFooter( $"You are #{response.PlayerRank + 1} of {response.TotalCount}" );
}
Example with the npm packages
const page = await client.getLeaderboard("weekly_arena", { Window: "AroundPlayer", Limit: 10 });
for (const entry of page.Entries) {
const place = entry.Rank >= 0 ? `#${entry.Rank + 1}` : "-";
const owner = entry.IsSelf ? "You" : entry.OwnerDisplayId !== "" ? entry.OwnerName : "Unknown user";
addRow(place, owner, entry.Score, entry.OwnerDisplayId);
}
if (page.PlayerRank >= 0) {
showFooter(`You are #${page.PlayerRank + 1} of ${page.TotalCount}`);
}
const friends = await client.getLeaderboard("weekly_arena", { GroupMoniker: "friends", Window: "Top", Offset: 0, Limit: 20 });
OwnerDisplayId is what a row action such as "whisper" or "view profile" addresses. Your app knows which of its boards your server writes for owners that are not users, such as clan_points in Run a clan-versus-clan board, and shows their rows from Metadata only, never from the owner fields (see Concepts). NameFromMetadata stands for reading the name your server stored in the metadata. On a board of users, an empty OwnerDisplayId means the owner could not be looked up in time or no longer exists.
Failure reasons of a view
| FailureReason | Cause | Client action |
|---|---|---|
Parameter 'LeaderboardKey' must be 1-128 characters |
The key is empty, only whitespace or too long. | Fix the key. |
Parameter 'GroupMoniker' cannot exceed 128 characters |
The group name is too long. | Fix the name. |
Parameter 'Offset' cannot be negative |
Offset is below 0. |
Send 0 or more. |
Parameter 'Limit' must be 1-100 |
Limit is outside 1-100. |
Send 1-100. |
Unknown leaderboard '<key>'. |
No board has this key. | Check the key; for a new board, try again a few minutes later. |
Unknown group '<name>'. |
The group lookup failed. | Try again later. |
Group is too large to rank (at most 100 members). |
The group and the user are more than 100 users. | Use a smaller group. |
Invalid read. |
The read was refused. | Do not repeat it unchanged. |
Unknown leaderboard or cycle. |
The board was deleted moments ago. | Stop using the key. |
Leaderboards are temporarily unavailable. |
A temporary failure. | Try again later. |
Read the user's own record (ClientGetLeaderboardRecordRequest)
Client -> server, any time after ServerWelcome. Reply: ServerAck, then ServerGetLeaderboardRecordResponse.
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | 1-128 characters, exact and case-sensitive | The board's key. |
CycleId |
text | Empty for the current cycle | The cycle to read. |
MessageId |
GUID | New for every message | Echoed in ResponseToMessageId. |
IsAckRequested |
true/false | true asks for a ServerAck, which comes as soon as the server has the request, before the typed response. |
Found = falseis not a failure:Successistrueand the user has no record in that cycle, or the cycle id is unknown.Entry.Rankis the user's rank on the whole board in the current cycle; it is -1 for a closed cycle, and can be -1 for a few seconds during a temporary disruption.- For the current cycle, when the user has a record, this read builds the board's live ranking.
ServerGetLeaderboardRecordResponse fields
| Field | Type | Rules | Meaning |
|---|---|---|---|
LeaderboardKey |
text | The key you sent. | |
CycleId |
text | The CycleId you sent; empty when you asked for the current cycle. |
|
Found |
true/false | true when the user has a record in the cycle. |
|
Entry |
LeaderboardEntry |
Set only when Found is true |
The user's record. |
ResponseToMessageId |
GUID | Your MessageId. |
|
Success |
true/false | false when the read was refused. |
|
FailureReason |
text | Empty on success | Why it was refused. |
Example
var mine = new ClientGetLeaderboardRecordRequest { LeaderboardKey = "weekly_arena" };
connection.Send( mine );
void OnServerGetLeaderboardRecordResponse( ServerGetLeaderboardRecordResponse response )
{
if( !response.Success )
ShowError( response.FailureReason );
else if( !response.Found )
ShowBest( "No score this week" );
else if( response.Entry.Rank >= 0 )
ShowBest( $"Best {response.Entry.Score}, #{response.Entry.Rank + 1}" );
else
ShowBest( $"Best {response.Entry.Score}" );
}
Example with the npm packages
const mine = await client.getLeaderboardRecord("weekly_arena"); // the current cycle; pass a cycle id for another
if (!mine.Found || !mine.Entry) {
showBest("No score this week");
} else if (mine.Entry.Rank >= 0) {
showBest(`Best ${mine.Entry.Score}, #${mine.Entry.Rank + 1}`);
} else {
showBest(`Best ${mine.Entry.Score}`);
}
Failure reasons of a record read
| FailureReason | Cause | Client action |
|---|---|---|
Parameter 'LeaderboardKey' must be 1-128 characters |
The key is empty, only whitespace or too long. | Fix the key. |
Unknown leaderboard '<key>'. |
No board has this key. | Check the key; for a new board, try again a few minutes later. |
Leaderboards are temporarily unavailable. |
A temporary failure. | Try again later. |
One record (LeaderboardEntry)
An object inside ServerGetLeaderboardResponse.Entries and ServerGetLeaderboardRecordResponse.Entry. It has no MessageId or response fields.
| Field | Type | Rules | Meaning |
|---|---|---|---|
OwnerId |
text | The unique user id on the user's own record; empty on every other record | The owner's unique user id, only on the connected user's own record (IsSelf true). Another user's unique user id is never sent: identify other users by OwnerDisplayId and OwnerName, and records of other owners, such as a clan, by Metadata. |
Score |
whole number | The stored score. 0 for a group member without a record. | |
Subscore |
whole number | The stored subscore. 0 when none was submitted. | |
Rank |
whole number | -1 when not ranked | Zero-based position within the scope of the read. -1 for a single read of a closed cycle and for a group member without a record. |
Metadata |
text | Empty when none | The stored metadata. |
OwnerDisplayId |
text | Empty when the owner is not a user | The display id of the user who owns the record, as it is now, on every record owned by a user of the application, the user's own included. Use it to show the owner and to address them, for example in a whisper. Empty for a record of another owner, such as a clan, and when the owner could not be looked up in time. |
OwnerName |
text | Empty when OwnerDisplayId is empty |
The name of the user who owns the record, as it is now. |
IsSelf |
true/false | true on the connected user's own record, the only record that carries OwnerId; false on every other record. |
Choose the window (LeaderboardWindow)
An enum used in ClientGetLeaderboardRequest.Window and echoed in ServerGetLeaderboardResponse.Window.
| Name | Meaning |
|---|---|
AroundPlayer |
The page around the user's own record; the top when the user has none. |
Top |
The page starting at Offset, counted from the best record. |
Failure reasons
All FailureReason texts of the three leaderboard responses. The first five are rules of the request itself: a request that breaks one is refused before the board is read or written, and its ServerAck, when you asked for one, carries Success false and the same text. The contracts library checks them before sending, and the connection is closed after more than 10 refused messages within 10 seconds (see Replies and acknowledgements).
| FailureReason | Responses | Cause | Retry? |
|---|---|---|---|
Parameter 'LeaderboardKey' must be 1-128 characters |
all three | The key is empty, only whitespace or longer than 128 characters. | No. |
Parameter 'Metadata' cannot exceed 4096 characters |
submit | Metadata is longer than 4096 characters. |
No. |
Parameter 'GroupMoniker' cannot exceed 128 characters |
view | The group name is longer than 128 characters. | No. |
Parameter 'Offset' cannot be negative |
view | Offset is below 0. |
No. |
Parameter 'Limit' must be 1-100 |
view | Limit is outside 1-100. |
No. |
Unknown leaderboard '<key>'. |
all three | No board has this key. A new board reaches clients within about 2.5 minutes. | Only for a new board, after a few minutes. |
Client submission is not allowed on this leaderboard. |
submit | Allow client submission is off on the board. | No. |
Invalid submission. |
submit | Refused by the board's rules: the score is above the Max-score bound or outside ±2^53, or the unique user id is longer than 128 characters. | No. |
Too many submissions. Try again shortly. |
submit | The user's submission limit for the current 10-second window is used up. | Yes, in the next window. |
Unknown group '<name>'. |
view | The group lookup failed. | Yes, later. |
Group is too large to rank (at most 100 members). |
view | The group plus the user are more than 100 users. | No. |
Invalid read. |
view | The read was refused by the server. | No. |
Unknown leaderboard or cycle. |
submit, view | The board was deleted moments ago. | No. |
This message was already used for a different submission. |
submit | The MessageId was used by this user in the last 24 hours for a submission with other content. | No. Use a new MessageId for a new submission. |
Leaderboards are temporarily unavailable. |
all three | A temporary failure. | Reads: yes, later. Submissions: resend unchanged with the same MessageId after 5 minutes (see After a lost response). |
Invalid submission. does not say which rule failed. Check the score against the board's Max-score bound first.
After a lost response
When the connection drops before the typed response arrives, no typed response comes in time (a ServerAck without it says only that the server received the submission), or Leaderboards are temporarily unavailable. comes back, the client cannot tell whether a submission was applied. Send the same message again, unchanged, with the same MessageId:
- Never applied twice. For 24 hours, in every region, the server recognises a submission by the user and its
MessageId. A resend of a submission that was applied gets the typed response of that first submission (Accepted,ScoreandCycleIdas they were,Rankas it is now) and changes nothing. A resend of a submission that was never applied is processed as a normal submission. - Within 5 minutes, only an acknowledgement. The server also drops a
MessageIdit saw in the same region in the last 5 minutes, and every repeat restarts the 5 minutes: such a resend gets at most aServerAckand no typed response. TheServerAckcomes as soon as the server receives a submission, before its typed response: when the original got one, the server has the submission, so wait until 5 minutes have passed since your last send and send the message once more to learn the outcome. When noServerAckcame, the submission may not have arrived: resend at once, at most 3 times within 5 minutes. Repeating oneMessageIdmany times can get the connection closed. See Message ids and duplicates. - The submission limit applies to resends. A resend counts against the user's submission limit like any submission. When the limit of the current 10-second window is used up, it gets
Too many submissions. Try again shortly.instead of the first result; send it again, unchanged, in a later window. - Only unchanged resends. The same
MessageIdwith another board, score, subscore or metadata is refused withThis message was already used for a different submission.. - A new
MessageIdis a new submission. On Increment and Decrement boards it adds the score again. After 24 hours, even the sameMessageIdis a new submission. - With the npm packages 2.0,
submitScorerejects with aRequestTimeoutErrorwhen no typed response comes in time, with aConnectionClosedErrorwhen the connection drops first, and with aRequestFailedErrorforLeaderboards are temporarily unavailable.In all three cases callsubmitScoreagain with the same arguments and{ MessageId }:error.messageIdof theRequestTimeoutError, or theMessageIdyou made withnewMessageId()and passed, which also covers the other two. Theacknowledgedof aRequestTimeoutErrorsays whether the server acknowledged the submission. When it is false, the submission may not have arrived: resend at most 3 times within 5 minutes. When it is true, the server has it, and a resend within 5 minutes of the previous send gets only another acknowledgement and times out again: wait until 5 minutes have passed since the last send, then send it once more.
Moderation and recovery
Removing records
- From your server:
DELETE /api/v1/leaderboards/{leaderboardId}/records/{ownerId}removes one owner's record from any cycle, closed cycles included (?cycleId=). See Remove a record (DELETE /api/v1/leaderboards/{leaderboardId}/records/{ownerId}). - In the dashboard: on the board's Standings tab, while the current cycle is shown, the bin icon on a row removes that owner's record. Evict selected removes the selected rows, and Evict by owner id also takes owner ids you type (separated by commas or spaces). Each owner is removed with a separate request, and the dialog reports the ones that failed. The dashboard treats closed cycles as read-only: removing a record from a closed cycle is possible only through your server.
- A removal takes effect at once: the records below move up one place (in other regions within 1 to 2 seconds). The owner can submit again right away; ban the user as well when they should not.
- Removing a user from every board means one removal per board and per cycle: see Remove a cheater from every board and cycle.
- Deleting a user from your application, banning them or letting their account expire does not remove their leaderboard records. When a user's data must be erased, remove their records with the same per-board, per-cycle loop.
Deleting a board
The board's Danger zone tab deletes the board after you type its key to confirm. Your server deletes a board with DELETE /api/v1/leaderboards/{leaderboardId} (Application.Leaderboards.Write; see Delete a board), with the same effect.
After deleting a board, remove its id from your server's configuration. Clients that still use its key get Unknown leaderboard '<key>'. (or, for a few minutes, Unknown leaderboard or cycle.).
Plan allowances
| Allowance | Free | Subscription |
|---|---|---|
| Boards per application | 5 | 100 |
| Leaderboard writes per calendar month | 10,000 | 1,000,000 |
| Client submissions per user ("Messages Per Second Per User" × 10 per 10-second window) | 10 per 10 seconds | 10 per 10 seconds |
- Writes are metered, not refused. Every processed submission counts as one write, whether it comes from your server or a client and whether or not it changed the record (a Best score that is not better counts). Refused submissions (
400,403,404,422, and client submissions refused by the board or the rate limit), repeats of a keyed submission that was already applied (the sameIdempotency-Key, or the same clientMessageId, within 24 hours), removals and reads do not count. Going over the monthly allowance does not block writes. - Boards per application is enforced. Creating a board, in the dashboard or with
POST /api/v1/leaderboards, when the application already has as many boards as its plan allows is refused with403and"This plan allows <n> leaderboards per application. Delete a leaderboard or upgrade the plan to create more.". Deleting a board frees its place. Boards that already exist keep working when a plan change leaves the application over its allowance. - Client submissions are enforced as described in Submit a score (ClientSubmitScoreRequest). Your server's writes are not limited per user; only the edge limit of 5 requests per second per IP address applies.
Common patterns
| You want | Sort direction | Operator | Reset cadence | Allow client submission | Posted by your server with an Idempotency-Key | Posted by the client |
|---|---|---|---|---|---|---|
| Classic high-score table | Higher is better | Best | All-time | Off | Yes; retries are safe | Only with the setting on and a tight Max-score bound |
| Best lap time | Lower is better | Best | All-time or Monthly | Off | Yes | Only with the setting on and a tight Max-score bound |
| Weekly challenge | Higher is better | Best | Weekly | Off | Yes | Only with the setting on and a tight Max-score bound |
| Season points that accumulate | Higher is better | Increment (see Exactly-once totals) | Monthly or Yearly | Off | Yes; a keyed retry is applied once | Not recommended: any client could add any amount to its own user |
| User rating that moves up and down | Higher is better | Set | All-time | Off | Yes | Not recommended: any client could set any rating |
| Casual daily board just for fun | Higher is better | Best | Daily | On, with a tight Max-score bound | Optional | Yes |
| Clan or team points | Higher is better | Increment | Weekly | Off | Yes, with the clan's id as ownerId |
Not possible: a client writes only its own user's record |
End-to-end walkthrough
A weekly arena board: your server posts match results, clients show the standings, and your server announces the winners.
- You, in the dashboard: create a board with key
weekly_arena, display name "Weekly arena", "Higher is better", Best, Weekly, Allow client submission off, Max-score bound100000. Copy the id from the page address; the steps below write it as<leaderboard id>. - You, in the dashboard: create an API key with "Leaderboards: read" and "Leaderboards: write" and store its secret on your server.
- Your server:
POST /api/v1/api-clients/authenticatewith the application id and the key's secret. Success:200with atoken. - Your server, after a match:
POST /api/v1/leaderboards/<leaderboard id>/recordswithIdempotency-Key: match-8812:<unique user id>and{"ownerId":"<unique user id>","score":18250}. Success:200with{"accepted":true,"score":18250,"rank":null,"cycleId":"2026-W40"}. The rank isnullbecause the new cycle has not been read since its first record, so its ranking is cold. - Client: sends
ClientGetLeaderboardRequestwithLeaderboardKey = "weekly_arena",Window = AroundPlayer,Limit = 10. Success:ServerGetLeaderboardResponsewithSuccess = true,CycleId = "2026-W40",PlayerRank = 0,TotalCount = 1and the user's entry. This read built the ranking. - Your server: the next
POSTfor another user answers with arank, for example1. - Five Minutes: on Monday at 00:00 UTC the cycle
2026-W41starts empty; new writes land there. - Your server:
GET .../cyclesanswers2026-W41(current) and2026-W40;GET .../records?cycleId=2026-W40&limit=3answers the three winners with ranks 0, 1 and 2. - Your server: a winner turns out to have cheated:
DELETE .../records/<unique user id>?cycleId=2026-W40answers204, and the next read of2026-W40shows the new top three.
Limits
| Limit | Value | What happens when exceeded |
|---|---|---|
| Board key | 1-128 characters, unique in the application regardless of letter case | The dashboard does not create the board; the Server API answers 400 for a key outside the length, 409 for a taken key. |
| Owner id | 1-128 characters | 400 over REST; Invalid submission. on the wire. |
Records per page (limit, Limit) |
1-100, default 50 | REST clamps to 1-100; the wire refuses with Parameter 'Limit' must be 1-100. |
| Offset | 0 or more | REST treats a negative value as 0; the wire refuses with Parameter 'Offset' cannot be negative. |
| One submitted score | Between -2^53 and 2^53 | 400 over REST; Invalid submission. on the wire. |
| Max-score bound | Per board, 0 or more | 400 over REST; Invalid submission. on the wire. |
| Stored total | Not checked; keep within ±2^53 | Ranks can be inexact beyond ±2^53; beyond the 64-bit range the write fails. |
| Metadata from a client | 4096 characters | Parameter 'Metadata' cannot exceed 4096 characters |
| Metadata from your server | Not enforced; keep to 4096 characters | Clients receive whatever you stored. |
ownerIds in one request |
About 100 ids of 64 characters (the URL must stay under about 8 KB) | The request is refused. |
| Group view | 100 users, including the user | Group is too large to rank (at most 100 members). |
GroupMoniker |
128 characters | Parameter 'GroupMoniker' cannot exceed 128 characters |
| Client submissions | "Messages Per Second Per User" × 10 per 10-second window per user (10 on Free and Subscription) | Too many submissions. Try again shortly. |
| Server API requests | 5 per second per IP address and region | 429 or 503 from the edge. |
Idempotency-Key |
1-128 characters; a keyed write is applied at most once for 24 hours, in every region | 400 for a longer key. |
Duplicate MessageId |
Dropped for 5 minutes after its last send, per region | Later, or in another region, the message is processed again; a submission that was already applied gets its first result and is not applied again (24 hours). |
| Live ranking | Per region: built by a read of a cycle that has records; dropped after 6 hours without reads there or score changes anywhere; writes through other regions arrive within 1 to 2 seconds | rank null or -1 on writes until the next read in that region. |
| New key reaches clients | About 2.5 minutes | Unknown leaderboard '<key>'. until then. |
| Setting changes take effect | Usually 1 minute, at most about 2 minutes | Writes use the previous settings until then. |
| Boards per application | Free 5, Subscription 100 | 403 on a create beyond it. |
| Leaderboard writes per calendar month | Free 10,000, Subscription 1,000,000 | Metered, not refused. |
The npm packages do not send a message that breaks the limit of a board key, Limit, Offset, GroupMoniker or client Metadata: the call rejects with a MessageValidationError that carries the refusal named here (see Client prerequisites).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Every REST call answers 404 with an empty body |
The path carries the board key instead of the id, or the id of another application's board. | Use the GUID from the board's dashboard page address or from GET /api/v1/leaderboards, with an API key of the same application. |
GET .../records/{ownerId} answers 404 |
The owner has no record in that cycle, or the board id is wrong. | List the cycles: 404 there means the board id is wrong. |
rank is null in a POST answer |
The board's ranking is cold in the region that processed the write: the current cycle has not been read there since its first record, or has been neither read there nor changed in the last 6 hours. | Treat the rank as optional, or read the owner's record, which builds the ranking and returns the rank. |
rank is null when reading a record |
The cycle is closed, or a brief disruption. | Closed cycles have no single-record ranks; use a page read. Otherwise repeat after a second. |
Unknown leaderboard '<key>'. right after creating a board |
New keys reach clients within about 2.5 minutes. The key is also case-sensitive. | Wait a few minutes; compare the key exactly. |
403 on GET with the key that submits scores |
The API key holds Application.Scores.Write only; Write does not imply Read. |
Add Application.Scores.Read ("Leaderboards: read") to the key and get a new token. |
403 on POST or DELETE |
The API key lacks Application.Scores.Write. |
Add "Leaderboards: write" to the key and get a new token. |
403 on POST /api/v1/leaderboards, or on PUT or DELETE /api/v1/leaderboards/{leaderboardId} |
The API key lacks Application.Leaderboards.Write ("Leaderboards: manage boards"); "Leaderboards: write" does not include it. |
Create a key with "Leaderboards: manage boards" for the server that sets up boards, and get a new token with it. |
403 "This plan allows <n> leaderboards per application. ..." on POST /api/v1/leaderboards, or the same reason in the dashboard |
The application already has as many boards as its plan allows. | Delete a board you no longer need, or upgrade the plan. |
409 on POST |
A request with the same Idempotency-Key is still running in the same region. |
Retry after Retry-After with the same key and body. |
409 "This leaderboard holds records, so its scoring rules are locked: ..." on PUT /api/v1/leaderboards/{leaderboardId}, or the same reason in the dashboard |
The board holds records in some cycle, and the change touches the rules the reason names. | Send the operator, sort order and reset cadence unchanged; to use other rules, create a new board or empty this one (see Locked scoring rules). |
| The dashboard offers the scoring rules but refuses to save them | The board received its first record after the page was opened, or the page could not tell whether the board holds records. | As above: the rules are locked while any cycle holds records. After the refusal the page greys them out; Title, Allow client submission and the Max-score bound still save. |
422 on POST |
The Idempotency-Key was used for another body or another board. |
Build keys from match id, board and owner; do not reuse a key for a different write. |
Bursts of 429 or 503 |
Your servers send more than 5 requests per second from one IP address. | Pace bulk jobs at 5 per second or less and retry with backoff. |
400 "The stated operator disagrees..." |
operator in the body differs from the board's operator. |
Leave operator out, or correct it. |
A Best submission answers accepted: false |
The score is equal to or worse than the stored score. Equal is not an improvement. | Nothing to fix. |
| Subscore or metadata disappeared | On Set, Increment and Decrement boards every write replaces them; omitted values store 0 and null. |
Send subscore and metadata with every write. |
| Ties are not ordered by subscore | The subscore is never used for ordering; ties are ordered by owner id. | Fold the tie-breaker into the score. |
Invalid submission. on a client |
The score is above the Max-score bound or outside ±2^53, or the unique user id is longer than 128 characters. | Check the bound in the dashboard. Unique user ids longer than 128 characters cannot hold records. |
Client submission is not allowed on this leaderboard. |
Allow client submission is off, or was turned on less than about 2 minutes ago. | Turn it on and wait, or submit through your server. |
Too many submissions. Try again shortly. |
The user used up the submissions of the current 10-second window, across all boards. Resends count too. | Submit once per result; wait for the next window. |
A resent ClientSubmitScoreRequest gets only a ServerAck |
The same MessageId reached the same region less than 5 minutes after its last send and was dropped as a duplicate. |
Wait until 5 minutes have passed since the last send, then send it once more, unchanged. |
With the npm packages, a leaderboard call rejects at once with MessageValidationError. |
The message breaks a rule of its table, such as an empty key, Limit outside 1-100 or Metadata over 4096 characters; error.field names the field, and the error's message is the FailureReason the tables list. Nothing was sent. |
Keep to the rules in Client messages. |
This message was already used for a different submission. |
A MessageId was reused for a submission with other content. |
Give every new submission a new MessageId. |
Clients see an empty OwnerId on other users' entries |
Only the user's own record carries its owner id; another user's unique user id is never sent. | Show and address other users by OwnerDisplayId and OwnerName; find the user's own row by IsSelf. |
An entry has an empty OwnerDisplayId |
The owner is not a user of the application, such as a clan your server wrote a record for, or the owner could not be looked up in time. | Store what clients should show for other owners in the record's metadata. |
A clan's row carries a user's OwnerDisplayId and OwnerName |
A client logged in with the clan's owner id while Require authenticated sessions was off, which created a user with that id. | Show boards of clans from Metadata only. Move the clan to an id nobody can guess: write its record under the new id and remove the old record. |
| A group view shows only the user | The name matches none of the user's own personal groups, or the group has no members. | Check the group name and that the group belongs to the connected user. |
Group members appear last with Rank -1 and Score 0 although they have scores |
Their records are stored under other owner ids, or in another cycle. A group view looks members up by their unique user id. | Write users' records with the unique user id as ownerId. |
A read with friendsOf or groupId returns the whole board |
These parameters do not exist and are ignored. | Pass the roster as repeated ownerIds. |
An ownerIds read returns no entries and a single id in unrankedOwnerIds |
The ids were sent as one comma-separated value, which counts as one id. | Repeat ownerIds once per id. |
| A score counted twice on an Increment board | A retry without an Idempotency-Key, a retry more than 24 hours after the first attempt, or a client resend with a new MessageId. |
Send an Idempotency-Key with every write and repeat it on every retry; resend an unanswered client submission with the same MessageId. |
| A new cycle is empty | The reset cadence started a new cycle; nothing carries over. | Read the previous cycle with cycleId. |
| Old records gone after re-creating a deleted board | The new board has a new id; the old records belong to the deleted board. | Nothing can bring them back; see Deleting a board. |
Related documents
- Five Minute Server API 1.0: hosts, API keys, the server token, scopes, errors, rate limits and idempotency.
- Five Minute wire protocol 2.0: connecting a client, the login handshake and message conventions.
- Five Minute Player Authentication 1.0: signing users in and the unique user id used as
ownerId. - npm packages 2.0: the JavaScript client, with
submitScore(),getLeaderboard()andgetLeaderboardRecord(). - Five Minute API Contracts 2.0: the generated field reference of every wire message and type.
- Five Minute Remote Config 1.0: remote config, experiments and metric events.
