Integration Examples
Working recipes against the real API. Swap in your host, port, and Bearer key and each one runs as-is.
A tiny API client
One helper covers every recipe below: it prefixes the base URL, attaches the Bearer key, unwraps JSON, and throws on non-2xx.
statfyr-client.jsjavascript
export function createClient({ baseUrl = "http://localhost:8080", apiKey = "" } = {}) {
async function get(path) {
const res = await fetch(`${baseUrl}${path}`, {
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json();
}
return {
health: () => get("/api/health"),
players: (q = "") => get(`/api/players${q}`),
playerStats: (id, q = "") => get(`/api/player/${id}${q}`),
playerSummary: (id, q = "") => get(`/api/player/${id}/summary${q}`),
leaderboard: (stat, q = "") => get(`/api/leaderboard/${stat}${q}`),
};
}Live dashboard
Poll the summary endpoint every 30 seconds, which matches the plugin's cache TTL, so no request ever pays for a full statistic re-read.
import { createClient } from "./statfyr-client.js";
const api = createClient({ baseUrl, apiKey });
async function renderPlayer(uuid) {
const s = await api.playerSummary(uuid);
document.querySelector("#playtime").textContent = s.playtime_formatted;
document.querySelector("#kills").textContent =
`${s.combat.player_kills} players · ${s.combat.mob_kills} mobs`;
document.querySelector("#distance").textContent =
`${s.movement.total_distance_km.toFixed(1)} km walked`;
document.querySelector("#mined").textContent = s.activity.blocks_mined.toLocaleString();
}
renderPlayer("069a79f4-44e9-4726-a5be-fca90e38aaf5");
setInterval(() => renderPlayer("069a79f4-44e9-4726-a5be-fca90e38aaf5"), 30_000);Leaderboard page
rank is computed as offset + position, so paging through with limit and page keeps continuous global ranks:
const api = createClient({ baseUrl, apiKey });
async function topPlaytime(page = 0, limit = 10) {
const lb = await api.leaderboard(
"playtime",
`?limit=${limit}&page=${page}&order=desc`,
);
for (const e of lb.entries) {
console.log(`#${e.rank} ${e.name} · ${e.formatted}`);
}
console.log(`page ${lb.page} of ${Math.ceil(lb.total / lb.limit)}`);
}
topPlaytime();Discord bot
A discord.js command that answers /top with the playtime leaderboard and can look up a player's summary by name:
bot.jsjavascript
import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
import { createClient } from "./statfyr-client.js";
const api = createClient({ baseUrl: process.env.STATFYR_URL, apiKey: process.env.STATFYR_API_KEY });
const discord = new Client({ intents: [GatewayIntentBits.Guilds] });
discord.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === "top") {
const lb = await api.leaderboard("playtime", "?limit=10");
const lines = lb.entries.map((e) => `**#${e.rank}** ${e.name} · ${e.formatted}`);
await interaction.reply({
embeds: [new EmbedBuilder()
.setTitle("Playtime leaderboard")
.setDescription(lines.join("\n"))],
});
}
if (interaction.commandName === "stats") {
const name = interaction.options.getString("player");
const s = await api.playerSummary(name);
await interaction.reply(
`${s.name}: ${s.playtime_formatted} played, ${s.combat.deaths} deaths, ${s.activity.blocks_mined} blocks mined`,
);
}
});
discord.login(process.env.DISCORD_TOKEN);Polling tips
- Align polling with the 30-second statistic cache: faster cycles burn your rate limit (120 requests / 60 s by default) without fresher data.
- Use
/api/healthfor uptime checks and/api/players?online_only=truefor live rosters. - Need one section?
/api/player/<id>/summary?movement=false&combat=falseslims the payload to activity only. - Send
Accept-Encoding: gzip(fetch and axios do by default); large raw dumps compress well.