Skip to content
DonutStatsAPI
Sign up
Concepts

Live lookups

Player stats can be refreshed on demand. The API waits a few seconds for the new numbers before it answers.

When you call GET /v1/players/{username} and the stored stats are more than 30 seconds old, the API starts a live refresh for that player. It waits up to 15 seconds, then answers with whatever is freshest. If the refresh finished in time you get brand new numbers. If not, you get the stored numbers and the refresh keeps going, so the next call is fresh.

Every answer includes a Lookup object that tells you what happened and when the stats were refreshed.

lookup
{
  "status": "skipped",
  "checkedAt": 1792722587957
}
lookup=What happens
autoThe default. Refreshes stats older than 30 seconds, then returns.
noneNever waits. Returns stored data immediately, or 404 if the player is not stored.
forceAlways refreshes, even if the stats are seconds old. Needs the force scope.
lookup.statusMeaning
freshThe stored stats were recent enough, so no refresh was needed.
updatedThe stats were just refreshed. These numbers are seconds old.
pendingThe refresh has not finished yet. Ask again in a few seconds.
not_foundDonutSMP has no player with this name. Remembered for 10 minutes.
rate_limitedYour lookup budget is spent. You got stored data.
unavailableLive refreshes are briefly unavailable. You got stored data.
skippedNo lookup was needed or allowed: lookup=none, fresh stats or a key without the lookup scope.

The first time anyone asks for a player, there is nothing stored to fall back on. If the refresh finishes within 15 seconds you get the full profile. If it does not, you get 202 with no player yet:

{
  "player": null,
  "lookup": {
    "status": "pending",
    "checkedAt": null
  }
}

Wait a few seconds and ask again. This helper does that for you:

async function player(username) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(
      `https://api.donutstats.io/v1/players/${encodeURIComponent(username)}`,
      { headers: { "x-api-key": process.env.DONUTSTATS_API_KEY } },
    );
    if (response.status === 202) {
      await new Promise((resolve) => setTimeout(resolve, 3000));
      continue;
    }
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`DonutStats API returned ${response.status}`);
    return (await response.json()).player;
  }
  return null;
}

Live refreshes are expensive, so each key can start a limited number per minute, separate from its request limit. When the budget is spent you still get stored data, marked rate_limited. Only when there is no stored data at all does the API answer 429:

{
  "error": "Lookup rate limit exceeded",
  "lookup": {
    "status": "rate_limited",
    "checkedAt": null
  }
}
  • Use the default auto when a person is looking at one profile.
  • Use lookup=none for anything in bulk, such as refreshing a list of players for a leaderboard of your own.
  • Check lookup.checkedAt before showing numbers as live.
  • Keep force for rare cases where seconds matter, like confirming a payment.

Lookups make the whole API better

Every lookup also refreshes the player's presence and what they are selling on the auction house, so their listings show up in the listings endpoint shortly after.