The endpoint index. Start here if you are exploring programmatically — it lists every available URL, so a client can discover the API without hard-coding paths.
One request, zero setup.
There is nothing to sign up for. Point any HTTP client at the base URL and you get JSON back. This returns the top ten jumpers on the server:
curl https://api.hydrosmp.com/v1/stats/jump?limit=10const res = await fetch('https://api.hydrosmp.com/v1/stats/jump?limit=10');
const body = await res.json();
for (const entry of body.data) {
console.log(`#${entry.rank} ${entry.player.name} — ${entry.display}`);
}import requests
r = requests.get("https://api.hydrosmp.com/v1/stats/jump", params={"limit": 10})
r.raise_for_status()
for entry in r.json()["data"]:
print(f"#{entry['rank']} {entry['player']['name']} — {entry['display']}")Every endpoint below has a Send request button that runs against the live API and shows you the real response. Nothing is mocked.
Base URL & versioning
Every endpoint lives under a version prefix. The current version is v1.
https://api.hydrosmp.com/v1
- No authentication. There are no API keys, tokens or headers to send. All of this data is already public on the stats site.
- GET and HEAD only. Anything else returns
405. The API cannot modify server data by design. - CORS is open.
Access-Control-Allow-Origin: *on every response, so browser-side tools and web overlays work without a proxy. - Additive changes only within v1. New fields and new endpoints can appear at any time, so parse defensively and ignore keys you do not recognise. Removing or renaming a field means a new version path (
/v2), and v1 stays up alongside it. - Machine-readable description. The full OpenAPI 3.1 spec is generated from the same definitions the server uses, so it cannot drift out of date.
Every response has the same shape.
Successful responses are always an object with data and meta. data is the thing you asked for — an object for a single resource, an array for a collection. meta carries everything about the response rather than in it: pagination, data freshness, and any extra context the endpoint provides.
{
"data": [ … ],
"meta": {
"data_updated_at": "2026-08-04T20:31:00.821Z",
"pagination": { "total": 433, "count": 50, "limit": 50, "offset": 0,
"next": "…?limit=50&offset=50", "previous": null }
}
}
meta.data_updated_at tells you when the statistics were last regenerated from the world save. The underlying data is pulled on a schedule, not per request, so this is the real age of what you are reading.
All timestamps everywhere are ISO 8601 in UTC. All identifiers are lowercase snake_case.
Players by name or UUID.
Anywhere the docs show {player}, you can pass any of these and the API resolves it for you — there is no separate lookup call:
| Form | Example | Notes |
|---|---|---|
| In-game name | Phobia0 | Case-insensitive. Resolves to the most recently seen account if a name was ever reused. |
| Dashed UUID | b6afdb4d-3c3e-4adf-a144-93525f8d3044 | The canonical form. Always safe. |
| Bare UUID | b6afdb4d3c3e4adfa14493525f8d3044 | Dashes are inserted for you. |
Players rename themselves. If your bot caches anything, cache the uuid and re-resolve the display name on read — a name that worked last month may point somewhere else or nowhere at all.
Java and Bedrock
Hydro runs full cross-play, so the player list mixes both. Every player object carries an edition field of java or bedrock. Bedrock players connect through Geyser and are given synthetic UUIDs that begin 00000000-0000-0000-0009-…; those UUIDs are not resolvable by Mojang or by third-party avatar services, which is why the API hands you a working textures.head URL for them instead of leaving you to work it out.
Values carry their own units.
Minecraft stores statistics in awkward native units — distance in centimetres, time in game ticks, damage in tenths of a heart. Rather than making you memorise which stat is which, every value in the API is returned as an object that describes itself:
{
"value": 148923044, // always the raw, native value
"unit": "cm",
"display": "1489.23 km", // ready to print in a Discord embed
"converted": { "metres": 1489230.44, "kilometres": 1489.23 }
}
| unit | Native meaning | converted |
|---|---|---|
| int | A plain count. | null |
| cm | Centimetres travelled. | metres, kilometres |
| ticks | Game ticks (20 per second). | seconds, minutes, hours |
| tenths_of_heart | Tenths of a half-heart of damage. | hearts |
Use display when you are showing a human a number, and value when you are doing arithmetic or storing it.
Pagination
Every collection endpoint takes limit and offset. The default page size is 50 and the maximum is 500; asking for more is silently clamped rather than rejected.
Instead of building the next URL yourself, follow meta.pagination.next — it is an absolute URL that preserves your other query parameters, and it is null on the last page.
let url = 'https://api.hydrosmp.com/v1/hall-of-fame?limit=100';
const everyone = [];
while (url) {
const body = await (await fetch(url)).json();
everyone.push(...body.data);
url = body.meta.pagination.next; // null ends the loop
}
Cache aggressively — we want you to.
The underlying statistics only change when the stats job runs against the world save, which is a scheduled task, not a live feed. Polling every second gets you the same bytes every time.
- Responses carry
Cache-Control: public, max-age=…and a strongETagkeyed to the data generation. - Send that ETag back as
If-None-Matchand you get a304 Not Modifiedwith an empty body — which does not cost you meaningfully against the rate limit and costs you nothing in bandwidth. Last-Modifiedmirrorsmeta.data_updated_at, so you can also just compare that field between polls./v1/healthis deliberately never cached — it is the one endpoint that tells you the truth about freshness right now.
curl -i https://api.hydrosmp.com/v1/hall-of-fame \
-H 'If-None-Match: "a1b2c3d4e5f6…"'
HTTP/2 304
Once every few minutes is plenty for a leaderboard bot. If you need to know the instant new data lands, poll /v1/health cheaply and only re-fetch the heavy endpoints when data_updated_at changes.
Rate limits
240 requests per minute per IP address. That is high enough that a normal Discord bot will never touch it, and low enough that one runaway loop cannot take the API down for everyone else.
Every response tells you where you stand, so you never have to guess:
| Header | Meaning |
|---|---|
| RateLimit-Limit | Requests allowed in the current window. |
| RateLimit-Remaining | Requests you have left. |
| RateLimit-Reset | Seconds until the window resets. |
| RateLimit-Policy | The policy in force, e.g. 240;w=60. |
| Retry-After | Sent only on a 429. Wait this many seconds. |
Going over gets you a 429 with a Retry-After header. Honour it — retrying immediately just burns the next window too. If your project genuinely needs a higher ceiling, ask in Discord rather than working around it.
Errors tell you what to fix.
Errors follow RFC 9457 Problem Details and are served as application/problem+json. Branch on type, which is stable; detail is written for a human and may be reworded.
{
"type": "https://api.hydrosmp.com/errors/not-found",
"title": "Resource not found",
"status": 404,
"detail": "No player matching 'Notch'. Accepts a Minecraft UUID (with or
without dashes) or an exact in-game name (case-insensitive).",
"instance": "/v1/players/Notch"
}
| Status | type | When |
|---|---|---|
| 400 | invalid-request | A query parameter is malformed or out of range. The response names it. |
| 404 | not-found | No such player, stat, event or endpoint. |
| 404 | unversioned-path | You forgot the /v1 prefix. The response tells you the corrected URL. |
| 405 | method-not-allowed | You used something other than GET or HEAD. |
| 429 | rate-limit-exceeded | Slow down; see Retry-After. |
| 500 | internal-error | Our fault. Please report it in Discord. |
The endpoints.
Twenty-one endpoints, all GET. Hit Send request on any of them to see live data from the server right now.
Service health and, more usefully, data freshness. data_age_seconds is how long ago the statistics were last regenerated, and data_stale flips to true if that job has not run in over six hours. Never cached — poll this cheaply to decide whether to re-fetch anything heavier.
The only live endpoint. Everything else on this page is a snapshot regenerated every 15 minutes; this one asks the game server directly, using the same Server List Ping your Minecraft client makes when it draws the server in your multiplayer list.
You get whether the server is up, the current and maximum player count, the running version, the MOTD as plain text, and the short random sample of online players the server itself publishes. Cached for 10 seconds, so a busy /online command will not open a socket per invocation.
If the server is restarting, you get online: false with null counts rather than an error status — so your bot can render "offline" without special-casing an exception.
Server addresses, player counts broken down by edition, and the rules the stats engine applies — the minimum playtime before a player is ranked, how long before they count as inactive, and the crown weights used to score the hall of fame.
The full catalogue of tracked statistics, each with its human title, description, unit, a link to its leaderboard, and the player who currently holds the record. This is the endpoint that tells you which stat_id values exist.
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Case-insensitive filter across id, title and description. ?search=mine finds every mining stat. |
| limitoptional | integer | Page size, 1–500. Defaults to 250 here so the whole catalogue fits in one call. |
| offsetoptional | integer | Records to skip. Default 0. |
The leaderboard for one statistic, ordered best first. Each entry carries the rank, the full player object and the value. meta.stat repeats the stat's title and unit so you can render a header without a second request.
| Parameter | Type | Description |
|---|---|---|
| stat_idrequired | string | An id from /v1/stats, e.g. jump, mine_diamond_ore, walk. |
| Parameter | Type | Description |
|---|---|---|
| limitoptional | integer | Page size, 1–500. Default 50. |
| offsetoptional | integer | Records to skip. Default 0. |
Every statistic summed across the whole server, with the per-player average. This is where the milestone posts come from — Hydro has collectively walked six figures of kilometres, and this endpoint is how you find that out.
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Case-insensitive filter on stat id or title. |
| limit / offsetoptional | integer | Standard pagination. Defaults to 250 so the whole set fits in one call. |
Every advancement anyone on the server holds, with how many players have it and how rare that makes them. Sorted rarest first by default, so the first page is the bragging-rights list — only four people have How Did We Get Here?.
Each entry also names the first player on the server to earn it, in first_by and first_completed_at. For the full ordered list of everyone who holds one, see /holders.
Datapacks file their own state as advancements — VanillaTweaks alone adds 16 that every single player "has". Those carry source: "datapack" and are excluded unless you pass ?include=datapack. Real in-game advancements are source: "vanilla", of which there are currently 126.
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Case-insensitive filter on id or title. |
| categoryoptional | string | One of story, nether, end, adventure, husbandry. The live list is in meta.categories. |
| sortoptional | string | rarity (default, rarest first), title, category, or first to order by which advancement was earned on the server earliest. |
| includeoptional | string | datapack to also return datapack bookkeeping entries. |
| limit / offsetoptional | integer | Standard pagination. Default 100. |
One advancement, with its rarity, wiki link and inventory icon. The namespace is optional — story/mine_diamond resolves to minecraft:story/mine_diamond.
| Parameter | Type | Description |
|---|---|---|
| advancement_idrequired | string | An id from /v1/advancements. Slashes are fine, no encoding needed. |
Everyone who holds one advancement, in the order they earned it. Position 1 is the first player on the server to get there — the roll of honour for Free the End starts with whoever killed the dragon first, back in July 2025.
Every entry in /v1/advancements carries first_by and first_completed_at, so you only need this endpoint when you want the full list rather than the pioneer.
| Parameter | Type | Description |
|---|---|---|
| advancement_idrequired | string | An id from /v1/advancements. Slashes are fine, no encoding needed. |
| Parameter | Type | Description |
|---|---|---|
| orderoptional | string | first (default, earliest earners first) or latest (most recent first). |
| limit / offsetoptional | integer | Standard pagination. Default 50. |
List and search every player the server has on record. Use search to build a name autocomplete, or ranked=true to get only the players who actually appear in leaderboards.
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Case-insensitive substring match on the player name. |
| editionoptional | string | java or bedrock. |
| rankedoptional | boolean | true restricts to players who met the minimum playtime and appear in rankings. |
| sortoptional | string | last_online (default, most recent first) or name (alphabetical). |
| limit / offsetoptional | integer | Standard pagination. Default 50. |
A player's profile card: identity, skin and avatar URLs, how many statistics they currently lead, their crown score and hall-of-fame position, and their ten best ranks. This is the one call a /stats <player> Discord command needs.
| Parameter | Type | Description |
|---|---|---|
| playerrequired | string | UUID or in-game name — see player identifiers. |
Every tracked statistic for one player, with their value and their rank on each. Pass ids when you only care about a handful — it is a much smaller response and a much faster embed.
| Parameter | Type | Description |
|---|---|---|
| idsoptional | string | Comma-separated stat ids, e.g. ?ids=jump,walk,mine_diamond_ore. An unknown id returns 400 rather than silently dropping it. |
rank is null where the player has a value but has not qualified for that leaderboard.
Advancement progress, newest completion first, with meta.completed and meta.total for a progress bar.
Minecraft files every unlocked crafting recipe as an advancement, which is roughly 1,600 entries of noise per player. Those are filtered out unless you explicitly ask for them with ?include=recipes. Expect a large response if you do.
| Parameter | Type | Description |
|---|---|---|
| includeoptional | string | Set to recipes to also include recipe unlocks. |
| completedoptional | boolean | Defaults to true (finished advancements only). Set false to also list ones still in progress. |
A player's journey rather than their scoreboard: when they first showed up, the 22 progression beats that mark a Minecraft playthrough and how many days after joining they hit each one, the rarest advancement they hold, and anything they were first on the server to reach.
Each milestone carries days_after_first_seen, which is what turns this into a timeline you can actually draw — the gap between Diamonds! on day 0 and Free the End on day 43 is the story.
It is derived from the oldest timestamp in the player's advancement file. Players who were already on the server when advancement tracking began have nothing older to point at, so their date sits on the start of the dataset. Those responses set first_seen_estimated: true — roughly 3% of the roster. Treat it as "playing since at least this date".
| Parameter | Type | Description |
|---|---|---|
| playerrequired | string | A UUID (dashed or bare) or an exact in-game name, case-insensitive. |
One player's hours played, sessions and deaths over the last 7 days, the last 30 days and since recording began — plus where they rank against everyone else this week. active: false means nothing has been recorded for them yet.
This endpoint returns totals, never a session timeline. The underlying data does know when each session started and ended, but publishing that would tell anyone reading the API when a given member is reliably asleep or away from home. Every individual fact in such a timeline is harmless; the pattern is not. So the raw per-window log is written outside every web root, on a path the API service is not permitted to read at all, and only the aggregates you see here are ever served.
These totals start when activity recording began. For playtime across the whole season, use the play statistic (/v1/players/{player}/stats?ids=play).
The crown ranking — players scored on how many statistics they place 1st, 2nd and 3rd in across the whole server. meta.crown_weights gives you the points each medal is worth, so you can show the maths.
| Parameter | Type | Description |
|---|---|---|
| limit / offsetoptional | integer | Standard pagination. Default 50. |
Server-wide "who just earned what", newest first. This is the one call behind an activity widget, a #advancements Discord channel or a "what happened while I was away" command.
Each entry carries the player, the advancement with its icon and rarity, and first_on_server — true when that unlock was the first time anyone on Hydro got it. Poll it with ?since= set to your last-seen timestamp and you have a notifier in about ten lines.
Datapacks file their own bookkeeping as advancements — every player "holds" the VanillaTweaks toggles — so only real in-game advancements appear here. The feed is also a rolling tail of the most recent unlocks rather than the full archive: meta.feed_window is how many events it covers and meta.oldest_in_feed is where it stops. For the complete history of one advancement use /holders.
| Parameter | Type | Description |
|---|---|---|
| playeroptional | string | Only this player's unlocks. UUID or exact name. |
| categoryoptional | string | One of story, nether, end, adventure, husbandry. |
| sinceoptional | string | Only unlocks at or after this time. ISO 8601 (2026-08-01T00:00:00Z) or epoch seconds. |
| limit / offsetoptional | integer | Standard pagination. Default 50. |
How much the server is actually being played, not just how many accounts exist. hour_of_day gives the average number of players online during each hour of the local clock — the real answer to "when is Hydro busy?" — and daily gives active players, hours played and deaths per day.
Minecraft's own play_time counter only advances while a player is online, and we copy those counters off the game server every 15 minutes. Diffing one pull against the last recovers exactly who was playing and for how long — no log parsing, no extra tracking. Resolution is therefore 15 minutes: a five-minute session still registers, but not the exact minute it started.
Like /v1/history, this begins at meta.recording_since. The counters were always there, but the changes between pulls were discarded before this existed and cannot be recovered.
Who has actually been playing lately. Hours, sessions and deaths per player, sortable, over the last 7 days, 30 days, or the whole recorded period. This is the "most active this week" list a leaderboard channel wants.
| Parameter | Type | Description |
|---|---|---|
| periodoptional | string | 7d (default), 30d or all (since recording began). |
| sortoptional | string | minutes (default), sessions or deaths. |
| limit / offsetoptional | integer | Standard pagination. Default 50. |
Weekly community events, newest first, each tied to a single statistic and carrying its current or final leader. Filter with ?active=true to show only what is running right now.
| Parameter | Type | Description |
|---|---|---|
| activeoptional | boolean | true for currently running events, false for finished ones. Omit for all. |
A single event by id, in the same shape as the list entries.
| Parameter | Type | Description |
|---|---|---|
| event_idrequired | string | An id from /v1/events, e.g. explorer_week6. |
Every Fabric mod the server runs, resolved against Modrinth: description, categories, supported Minecraft versions, download counts and icons. All of them are performance or quality-of-life — none change Vanilla gameplay.
The list is read from the same source that drives hydrosmp.com/mods, so the two can never disagree, and it is refreshed from Modrinth every few hours.
| Parameter | Type | Description |
|---|---|---|
| searchoptional | string | Case-insensitive filter on slug or name. |
A mod that Modrinth does not return is still listed, with resolved: false and null metadata, rather than silently disappearing.
The optional client modpack: its latest version, which Minecraft versions it supports, the changelog and the actual download files. Everything comes from Modrinth at fetch time — no version is ever hardcoded — so a /modpack bot command stays correct across updates without anyone editing it.
How busy the server has been over time, and how the roster has grown. Sampled every few minutes and rolled up into raw, hourly and daily series, each with average and peak concurrent players.
data.hour_of_day is the part most people want: average concurrent players for each hour of the local clock, which answers "when is Hydro actually busy?" without anyone guessing. data.peak holds the all-time high and when it happened.
Every other data file on the server is a snapshot of right now — the stats pull overwrites its output on each run, so nothing recorded a time series before this endpoint existed. The data therefore begins at meta.recording_since and grows from there. Earlier history was never stored and cannot be reconstructed.
| Parameter | Type | Description |
|---|---|---|
| resolutionoptional | string | hourly (default, last 30 days), raw (last 48 hours, every sample) or daily (all time). |
| limit / offsetoptional | integer | Standard pagination over the points. Default 168. |
When the ping fails, online is null rather than 0 — "we could not reach the server" is not the same as "nobody was playing", and averaging the two together would invent an outage-shaped dip. Each bucket reports how many such samples it saw in offline_samples.
Two players, every statistic either of them has, a winner on each one and an overall tally. One request is enough to render a full versus embed.
| Parameter | Type | Description |
|---|---|---|
| playersrequired | string | Exactly two comma-separated players, as UUIDs or names. ?players=Phobia0,Blxde7 |
| idsoptional | string | Restrict the comparison to these stat ids, for a tighter embed. |
Per-stat results are keyed by UUID under players, and winner holds the UUID that won (or null for a tie).
A random player who actually appears in the rankings, together with their single best rank. Good for a "player of the day" post. Never cached — a cached random is just a constant.
A random statistic with its top three players already attached. A daily-leaderboard bot is one call and one embed. Never cached.
The live player count as an image you can drop anywhere that renders pictures but cannot run code — a README, a forum signature, your own site. Returns image/svg+xml, not JSON.
| Parameter | Type | Description |
|---|---|---|
| labeloptional | string | Left-hand text. Default Hydro SMP. |
| coloroptional | string | Right-hand colour: blue, green, amber, red, purple, pink, grey, or a hex value like 1bd96a. Anything else falls back to the default rather than being echoed into the image. |
Any one of your stats as an inline badge. ?stat= takes an id from /v1/stats, so anything the server tracks can go in a signature.
| Parameter | Type | Description |
|---|---|---|
| statoptional | string | Any statistic id from /v1/stats (e.g. play, mine_diamond_ore), or one of the three synthetic views: advancements (default), medals, rank. |
| labeloptional | string | Override the left-hand text. Defaults to the player name. |
| coloroptional | string | As above. |
A typo in the player name returns a red not found badge with a real 404 status, rather than JSON. An <img> tag renders the body either way, so a broken embed still reads as a broken badge instead of a broken image icon.
A larger shareable card: playtime, advancements earned, hall-of-fame position and medal count, with the member-since line across the top.
| Parameter | Type | Description |
|---|---|---|
| coloroptional | string | Accent rail colour. Defaults to blue for Java players and pink for Bedrock. |
This service has no outbound network access by design, so it cannot fetch a head render to embed. Pointing at a remote image instead would not help: Discord, GitHub and every other embed sanitiser strips external references out of SVGs, so it would show as a broken box exactly where it matters. The card is pure vector and always renders.
Code examples.
A complete /stats command in the three places people usually build these.
const API = 'https://api.hydrosmp.com/v1';
client.on('interactionCreate', async (i) => {
if (!i.isChatInputCommand() || i.commandName !== 'stats') return;
const name = i.options.getString('player', true);
const res = await fetch(`${API}/players/${encodeURIComponent(name)}`);
if (res.status === 404) {
return i.reply({ content: `No Hydro player called **${name}**.`, ephemeral: true });
}
if (!res.ok) {
return i.reply({ content: 'The Hydro API is having a moment. Try again shortly.', ephemeral: true });
}
const { data: p } = await res.json();
await i.reply({ embeds: [{
title: p.name,
url: p.links.profile,
thumbnail: { url: p.textures.head },
color: 0x5bbfff,
fields: [
{ name: 'Crown score', value: String(p.crown_score), inline: true },
{ name: 'Hall of fame', value: `#${p.hall_of_fame_rank ?? '—'}`, inline: true },
{ name: 'Medals', value: `🥇 ${p.medals.gold} 🥈 ${p.medals.silver} 🥉 ${p.medals.bronze}`, inline: true },
{ name: 'Best ranks', value: p.top_ranks.slice(0, 5)
.map(s => `#${s.rank} · **${s.title}** — ${s.display}`).join('\n') || 'None yet' },
],
footer: { text: `Edition: ${p.edition} · last online ${p.last_online?.slice(0, 10) ?? 'unknown'}` },
}] });
});import aiohttp
import discord
from discord import app_commands
API = "https://api.hydrosmp.com/v1"
@tree.command(name="stats", description="Look up a Hydro SMP player")
@app_commands.describe(player="In-game name or UUID")
async def stats(interaction: discord.Interaction, player: str):
async with aiohttp.ClientSession() as session:
async with session.get(f"{API}/players/{player}") as res:
if res.status == 404:
await interaction.response.send_message(
f"No Hydro player called **{player}**.", ephemeral=True)
return
res.raise_for_status()
p = (await res.json())["data"]
embed = discord.Embed(title=p["name"], url=p["links"]["profile"], colour=0x5BBFFF)
embed.set_thumbnail(url=p["textures"]["head"])
embed.add_field(name="Crown score", value=p["crown_score"])
embed.add_field(name="Hall of fame", value=f"#{p['hall_of_fame_rank'] or '—'}")
embed.add_field(
name="Medals",
value=f"🥇 {p['medals']['gold']} 🥈 {p['medals']['silver']} 🥉 {p['medals']['bronze']}",
)
embed.add_field(
name="Best ranks",
value="\n".join(f"#{s['rank']} · **{s['title']}** — {s['display']}"
for s in p["top_ranks"][:5]) or "None yet",
inline=False,
)
await interaction.response.send_message(embed=embed)# Top 5 diamond miners, as a plain table
curl -s 'https://api.hydrosmp.com/v1/stats/mine_diamond_ore?limit=5' \
| jq -r '.data[] | "\(.rank)\t\(.player.name)\t\(.display)"'
# Who currently leads the running weekly event?
curl -s 'https://api.hydrosmp.com/v1/events?active=true' \
| jq -r '.data[] | "\(.title): \(.leader.player.name) — \(.leader.display)"'
# Every stat one player is ranked #1 in
curl -s 'https://api.hydrosmp.com/v1/players/Phobia0/stats' \
| jq -r '.data.stats[] | select(.rank == 1) | .title'Common recipes.
Call /v1/stats?search=… once at startup and cache the catalogue — it changes only when the server updates. Then hit /v1/stats/{id}?limit=10 per command.
Poll /v1/health. When data_updated_at moves, re-fetch the leaderboards you care about and diff the ranks against your last copy.
/v1/players?search=&limit=25 is fast enough to back a Discord autocomplete handler directly. Return the uuid as the option value.
CORS is open, so fetch straight from the browser source. Respect the cache headers and it costs the server almost nothing.
/v1/events?active=true gives you the running event and its current leader — post it on a schedule and let the API do the ranking.
Use textures.head as-is. It already points somewhere that works for both Java and Bedrock players, which hand-built Crafatar URLs will not.
Fair use & terms.
This API is provided free to the Hydro community. There are no keys to revoke, which means it runs on the assumption that people will use it reasonably. The rules are short:
- Build what you like. Discord bots, overlays, websites, spreadsheets, side projects — all fine, and all encouraged.
- Cache, and respect the headers. The data changes a few times a day at most. Hammering the endpoints in a tight loop is the one thing that would force us to lock this down.
- Identify yourself if you can. Setting a descriptive
User-Agent(a project name and a contact) means we can reach out about a problem instead of just blocking an IP. - Attribute the source. If your project is public, credit Hydro Vanilla SMP and link back to hydrosmp.com.
- Non-commercial. Do not resell this data or put it behind a paywall. Ask first if you want to do something commercial with it.
- Do not use it to target people. This data is about a Minecraft server. Using it to harass, dox or build dossiers on players is a Hydro rules violation like any other.
Everything this API returns is already published on stats.hydrosmp.com: in-game names, UUIDs, skins, playtime-derived statistics and advancement progress. There is no private, account, moderation or Discord data behind any endpoint, and no endpoint can write anything.
This is a community server run by volunteers, not a commercial service. Endpoints may be slow, briefly unavailable, or changed. Handle non-200 responses and stale data gracefully — and please report anything broken in Discord rather than retrying into the void.
Changelog
| Date | Version | Change |
|---|---|---|
| 2026-08-05 | v1.3.0 | Added /v1/activity (server-wide activity by hour and day), /v1/activity/players (most active players) and /v1/players/{p}/activity (per-player hours, sessions and deaths). Reconstructed by diffing playtime counters between stats pulls; totals only, never session timelines. |
| 2026-08-05 | v1.2.0 | Added /v1/feed (recent advancement unlocks), /v1/advancements/{id}/holders, /v1/players/{p}/milestones and /v1/history (population over time). Advancement entries now carry first_by and first_completed_at; player profiles now carry first_seen, days_on_server and advancements_earned. New SVG embeds: /v1/badge/server.svg, /v1/badge/player/{p}.svg and /v1/card/player/{p}.svg. |
| 2026-08-04 | v1.1.0 | Added /v1/status (live server ping), /v1/totals, /v1/advancements with rarity, /v1/mods, /v1/modpack, /v1/compare and the two /v1/random endpoints. Rate limit raised to 240/min. Fixed datapack recipe unlocks leaking into advancement responses. |
| 2026-08-04 | v1.0.0 | Initial public release. Twelve endpoints covering the stat catalogue, leaderboards, players, advancements, hall of fame and weekly events. |
Breaking changes will be announced in Discord before they ship, and will arrive as a new version path rather than a change to /v1.