Five Minute Leaderboards

Ranked boards with cycles, server-authoritative scores and moderation.

Product
Version

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, and Application.Scores.Write ("Leaderboards: write") to submit and remove records. Creating, changing and deleting boards needs Application.Leaderboards.Write ("Leaderboards: manage boards"), which Application.Scores.Write does 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?) and getLeaderboardRecord(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} and GET /leaderboards/{leaderboardId}/cycles need Application.Scores.Read; POST /leaderboards/{leaderboardId}/records and DELETE /leaderboards/{leaderboardId}/records/{ownerId} need Application.Scores.Write. Boards: GET /leaderboards and GET /leaderboards/{leaderboardId} need Application.Scores.Read; POST /leaderboards, PUT /leaderboards/{leaderboardId} and DELETE /leaderboards/{leaderboardId} need Application.Leaderboards.Write. Write does not imply Read, and neither write scope includes the other.
  • Server auth: POST /api/v1/api-clients/authenticate with {"applicationId":"<application id>","clientSecret":"<API key secret>"} returns {"token","expiresInSeconds":600,"scopes"}; send Authorization: Bearer <token>.
  • Client: protocol 2.0; messages ClientSubmitScoreRequest, ClientGetLeaderboardRequest, ClientGetLeaderboardRecordRequest and their typed responses; send only after ServerWelcome; a new MessageId per message; match replies on ResponseToMessageId.
  • Identifiers: leaderboardId is a GUID and the only way REST addresses a board (a key in its place answers 404); copy it from the last path segment of the board's dashboard page, or read it as id from GET /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 carries OwnerId (with IsSelf true), records of users carry OwnerDisplayId and OwnerName, and records of other owners, such as a clan, carry none of the three, so identify them by Metadata. An owner id your server chooses for a clan or team must be one no user can log in with, such as clan_ followed by 32 random hexadecimal characters, and boards of such owners are shown from Metadata only (see Concepts). cycleId: all, 2026, 2026-09, 2026-W40, 2026-09-29, 2026-09-29T14, 2026-09-29T14:30 or 2026-09-29T14:30:15 (UTC).
  • Limits: limit 1-100, default 50; ranks are 0-based; rank is null (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; wire Metadata at 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-Key header (1-128 characters) on POST .../records, and on POST /leaderboards when 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; retry 409, 429, 500, 502 and 503 with 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 unanswered ClientSubmitScoreRequest unchanged, 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 (409 on PUT /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 (use ownerIds); 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

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-A and clan-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 409 with 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; a startScore left out of the PUT body 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 order or start 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 503 with Retry-After: 5 and 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, and Unknown 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=..., or ClientGetLeaderboardRequest with CycleId) come in rank order with positional ranks.
  • Single reads of a closed cycle (GET .../records/{ownerId}?cycleId=..., or ClientGetLeaderboardRecordRequest with CycleId) return the record without a rank: rank is null over REST and Rank is -1 on the wire.
  • The whole-board AroundPlayer view of a closed cycle reports PlayerRank -1 and serves the top of the board. Use Window = Top with an Offset to 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, or Success = 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 answer 404 over REST and Found = false on the wire, and a removal answers 404.

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-b ranks above user-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-a ranks above user-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: rank is null over REST and Rank is -1 on 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:

  • rank is the position within the set, starting at 0.
  • totalCount is the number of owners in the set that have a record in the cycle; rosterCount is the number of distinct ids you sent. Duplicates are ignored.
  • On the last page, unrankedOwnerIds lists the ids that have no record in the cycle. On other pages it is empty.
  • offset and limit page 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 rank in 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 (null or -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, rank is the record's position on the whole board (offset + its index in entries) and totalCount is the number of records in the cycle. unrankedOwnerIds is [] and rosterCount is 0.
  • With ownerIds, ranks, totalCount, rosterCount and unrankedOwnerIds are as described in Ranking a subset.
  • rank is never null in a page.
  • metadataJson is a string (JSON text inside a JSON string) or null.
  • A read of the current cycle without ownerIds builds 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, groupId or aroundOwnerId do 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

  • rank is the zero-based position on the whole board in the current cycle. It is null for a closed cycle, and can be null for 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 404 does 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 all only, unless a cycle of an earlier cadence holds records.
  • Use it to check that a board id exists: it answers 404 only 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: accepted is false only on a Best board when the score is not strictly better than the stored one. score is the stored score after the write. cycleId is the cycle the write landed in. rank is the owner's zero-based position after the write, or null while the board's ranking is cold (see When no rank is returned).
  • Operator: leave operator out 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 subscore stores 0 and an omitted metadataJson stores null. 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 the rank as it was at the first write.
    • Any other retry, for example one served by another region or one after a 5xx, gets 200 without that header: accepted, score and cycleId of the first write, and the owner's rank as it is now (null while the board's ranking is cold in that region). A Best write that was accepted stays accepted: true on 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.
    • 400 and 404 answers 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 422 in 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.
  • Without a key every request is a new write, so a retry after a timeout or a 5xx can apply an Increment or Decrement score twice.
  • Plan: every processed write counts as one leaderboard write for your plan, whether or not accepted is true (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 cycleId it 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 example match-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 (after Retry-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, 404 or 422: 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 .../records with an Idempotency-Key is applied at most once for 24 hours after it was applied, in every region, also when the first attempt ended in a timeout or a 5xx. 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's accepted, score and cycleId.
  • Clients: the MessageId of a ClientSubmitScoreRequest is 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

  1. At start-up your server calls GET /api/v1/leaderboards with a token holding Application.Scores.Read. Success is 200 with every board of the application.
  2. For each key your app uses, it takes the board whose key matches exactly, with the same letter case, and keeps its id for the record routes.
  3. 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

  1. Your server gets a server token whose API key holds Application.Scores.Write (add Application.Scores.Read if you also read ranks), and caches it until shortly before expiresInSeconds runs out.
  2. Your server sends, for each user in the match, POST /api/v1/leaderboards/{leaderboardId}/records with ownerId set to the unique user id and Idempotency-Key: <match id>:<unique user id>. Success is 200.
  3. Your server reads accepted, score and rank from the answer. When rank is null, it reads the record with GET .../records/{ownerId} if it needs the rank.
  4. On 409, 429, 500, 502, 503 or 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.
  5. On 400, 403, 404 or 422, your server stops and logs the answer.

Show "you are here" from your server

  1. Your server calls GET /api/v1/leaderboards/{leaderboardId}/records/{ownerId}. 200 gives the user's rank r; 404 means the user has no record, so show the top of the board instead (GET .../records?limit=10).
  2. Your server computes offset = max(0, r - limit / 2) with integer division, for example r = 1204 and limit = 10 gives offset 1199.
  3. Your server calls GET .../records?offset=1199&limit=10. The user's record is in entries, and totalCount gives "1,205th of 58,310" (rank + 1 of totalCount).
  4. If rank in step 1 is null (a brief disruption), repeat step 1 after a second.

Rank a clan's members

  1. 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.
  2. Your server calls GET .../records?ownerIds=<id 1>&ownerIds=<id 2>&... with up to 100 ids.
  3. Your server shows entries with their relative ranks, then the ids in unrankedOwnerIds as "no score". totalCount is the number of members with a score and rosterCount the number of members asked for.

Run a clan-versus-clan board

  1. You create a board in the dashboard, for example key clan_points, operator Increment, "Higher is better", Weekly, Allow client submission off.
  2. 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 and metadataJson = what clients should show, such as "{\"clan\":\"Ember Wolves\"}", with an Idempotency-Key per match. Send the metadata with every write: on an Increment board every write replaces it.
  3. Clients read the board with ClientGetLeaderboardRequest and Window = Top, and show each clan from the entry's Metadata only. OwnerId, OwnerDisplayId and OwnerName are 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. PlayerRank is -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

  1. Your server calls GET .../cycles and takes the first entry with isCurrent: false: the most recent closed cycle that holds records. Check that it is the cycle you expect; a cycle without records is not listed.
  2. Your server calls GET .../records?cycleId=<that id>&limit=3. The entries are the winners with ranks 0, 1 and 2.
  3. Ranks are unique even for equal scores (ties are ordered by owner id). If equal scores should share a prize, compare score values yourself.
  4. 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

  1. Your server keeps the list of its board ids; there is no API that lists boards.
  2. For each board, your server calls GET .../cycles.
  3. For each cycle id, your server calls DELETE .../records/{ownerId}?cycleId=<cycle id>. 204 means removed; 404 means the user had no record there.
  4. 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

  1. The client sends ClientGetLeaderboardRecordRequest with LeaderboardKey, and CycleId empty for the current cycle.
  2. Five Minutes answers ServerGetLeaderboardRecordResponse. With Success = true and Found = true, show Entry.Score, and Entry.Rank + 1 when Entry.Rank is 0 or more (it is -1 for a closed cycle or during a brief disruption); with Found = 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

  1. The client sends ClientGetLeaderboardRequest with LeaderboardKey, Window = AroundPlayer and Limit = 10.
  2. Five Minutes answers ServerGetLeaderboardResponse with up to 10 entries around the user. PlayerRank + 1 of TotalCount gives "1,205th of 58,310". A user without a record gets the top of the board and PlayerRank -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

  1. The client keeps the user's friends in a personal group, for example friends, with ClientAddUsersToPersonalGroupRequest.
  2. The client sends ClientGetLeaderboardRequest with GroupMoniker = "friends" and Window = AroundPlayer.
  3. 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 by OwnerDisplayId and OwnerName; the user's own row has IsSelf true.

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 OwnerId is filled only on the user's own record, which also has IsSelf true; 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 Metadata stores 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 at max(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 than Limit entries.
  • Window = Top: the page starts at Offset.
  • A closed cycle has no live ranks: AroundPlayer reports PlayerRank -1 and serves the top. Use Top with Offset to page a closed cycle.
  • An unknown CycleId gives an empty page with Success = true and TotalCount 0.
  • 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 ClientAddUsersToPersonalGroupRequest or 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, PlayerRank and Offset are positions within the group, starting at 0. TotalCount is the number of group members, the user included, who have a record.
  • AroundPlayer centres the page on the user within the group; Top starts at Offset. Group views work on closed cycles too, and there PlayerRank is the user's position in the group.
  • Members without a record come after the ranked entries on the last page, with Rank -1, Score 0, Subscore 0 and empty Metadata. Like every other member they carry their OwnerDisplayId and OwnerName and no OwnerId. They are not counted in TotalCount, so the last page can hold more than Limit entries.
  • 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 = false is not a failure: Success is true and the user has no record in that cycle, or the cycle id is unknown.
  • Entry.Rank is 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, Score and CycleId as they were, Rank as 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 MessageId it saw in the same region in the last 5 minutes, and every repeat restarts the 5 minutes: such a resend gets at most a ServerAck and no typed response. The ServerAck comes 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 no ServerAck came, the submission may not have arrived: resend at once, at most 3 times within 5 minutes. Repeating one MessageId many 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 MessageId with another board, score, subscore or metadata is refused with This message was already used for a different submission..
  • A new MessageId is a new submission. On Increment and Decrement boards it adds the score again. After 24 hours, even the same MessageId is a new submission.
  • With the npm packages 2.0, submitScore rejects with a RequestTimeoutError when no typed response comes in time, with a ConnectionClosedError when the connection drops first, and with a RequestFailedError for Leaderboards are temporarily unavailable. In all three cases call submitScore again with the same arguments and { MessageId }: error.messageId of the RequestTimeoutError, or the MessageId you made with newMessageId() and passed, which also covers the other two. The acknowledged of a RequestTimeoutError says 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.

Deleting a board erases it with all of its records, in every cycle. The board id stops working within about 2 minutes; until then, your server's reads and writes by id can still succeed. The records are erased within a day, including any written in those minutes; until then they can no longer be reached through any API or the dashboard. Deleting cannot be undone: a new board with the same key gets a new id, starts empty and never shows the old records.

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 same Idempotency-Key, or the same client MessageId, 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 with 403 and "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.

  1. 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 bound 100000. Copy the id from the page address; the steps below write it as <leaderboard id>.
  2. You, in the dashboard: create an API key with "Leaderboards: read" and "Leaderboards: write" and store its secret on your server.
  3. Your server: POST /api/v1/api-clients/authenticate with the application id and the key's secret. Success: 200 with a token.
  4. Your server, after a match: POST /api/v1/leaderboards/<leaderboard id>/records with Idempotency-Key: match-8812:<unique user id> and {"ownerId":"<unique user id>","score":18250}. Success: 200 with {"accepted":true,"score":18250,"rank":null,"cycleId":"2026-W40"}. The rank is null because the new cycle has not been read since its first record, so its ranking is cold.
  5. Client: sends ClientGetLeaderboardRequest with LeaderboardKey = "weekly_arena", Window = AroundPlayer, Limit = 10. Success: ServerGetLeaderboardResponse with Success = true, CycleId = "2026-W40", PlayerRank = 0, TotalCount = 1 and the user's entry. This read built the ranking.
  6. Your server: the next POST for another user answers with a rank, for example 1.
  7. Five Minutes: on Monday at 00:00 UTC the cycle 2026-W41 starts empty; new writes land there.
  8. Your server: GET .../cycles answers 2026-W41 (current) and 2026-W40; GET .../records?cycleId=2026-W40&limit=3 answers the three winners with ranks 0, 1 and 2.
  9. Your server: a winner turns out to have cheated: DELETE .../records/<unique user id>?cycleId=2026-W40 answers 204, and the next read of 2026-W40 shows 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