QueryHost
Esc

Documentation menu

HTTP API

Public API

Query game servers over HTTPS without installing the library. Version 1 needs no key and allows calls from any origin.

Base URL

Every route lives under https://query.host/api/v1. There are no API keys. Query and games requests and responses are JSON, and queries are limited by IP address; status badges return SVG and use their own shared budget instead.

An OpenAPI 3.1 description at /api/v1/openapi.json covers every route, field, and error code, with the game list taken from the library's registry. Load it into Postman, Scalar, or a client generator, or hand it to an AI tool.

Query a server

POST /api/v1/query runs one live query, using the hosted cache. It accepts the same input as the library's query() function, and no other fields.

FieldTypeNotes
gamestringRequired. A supported game ID or alias.
hoststringRequired. A public hostname or IP address, not a URL.
portintegerGame port, 1 through 65,535. Required for generic a2s.
queryPortintegerOverrides the profile's query port.
modesummary or fullDefaults to full.
timeoutMsinteger1 through 5,000. Defaults to 5,000.
Query with curlshell
curl https://query.host/api/v1/query \
  -H 'Content-Type: application/json' \
  -d '{"game":"minecraft","host":"mc.example.com","mode":"summary"}'
Query from a browser or Node.jsTypeScript
const response = await fetch("https://query.host/api/v1/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ game: "rust", host: "203.0.113.5", port: 28015 }),
});

if (response.status === 429) {
  const seconds = Number(response.headers.get("Retry-After") ?? "1");
  // Wait before retrying; do not retry in a tight loop.
}

const result = await response.json();
if (result.ok) {
  console.log(result.data.players.online);
}

A query that reaches a server returns HTTP 200 with the library result, even when the server is offline or times out. Check ok in the body, as described in Result semantics. The added cache field and the x-queryhost-cache header say whether the result was live (miss), shared with an identical request (coalesced), or reused (hit).

Stream progress

Send Accept: application/x-ndjson to watch a query run. The response is newline-delimited JSON: a started line as each source begins, a completed line with the same report that appears in the result's sources, then one final result line with the body a JSON request would get.

Stream linesJSON
{"type":"started","source":"a2s-info"}
{"type":"completed","report":{"source":"a2s-info","status":"ok","rttMs":31}}
{"type":"started","source":"a2s-player"}
{"type":"completed","report":{"source":"a2s-player","status":"ok","rttMs":29}}
{"type":"result","result":{"ok":true,"game":"rust","cache":{"status":"miss"},...}}
Read the streamTypeScript
const response = await fetch("https://query.host/api/v1/query", {
  method: "POST",
  headers: {
    Accept: "application/x-ndjson",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ game: "rust", host: "203.0.113.5", port: 28015 }),
});

// Refusals such as 429 stay ordinary JSON.
if (response.headers.get("Content-Type")?.startsWith("application/x-ndjson")) {
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  let pending = "";
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
    const lines = (pending + chunk.value).split("\n");
    pending = lines.pop();
    for (const line of lines.filter(Boolean)) {
      const event = JSON.parse(line);
      if (event.type === "started") console.log("querying", event.source);
      if (event.type === "completed") console.log(event.report);
      if (event.type === "result") console.log(event.result.ok);
    }
  }
}

Cached and shared queries count the same as JSON ones. A cache hit streams only its result line, and a request sharing a running query first receives the lines it missed. Streams carry no Age or x-queryhost-cache header, so read cache from the result. Validation errors and 429 refusals are sent before the stream starts and stay ordinary JSON.

List games

GET /api/v1/games returns { games: [...] } with every supported game's ID, name, default ports, and capabilities, straight from the library's registry. Responses carry an ETag and may be reused for 5 minutes; send it back in If-None-Match to get an empty 304 when the list is unchanged.

Status badges

GET /api/v1/badge/{game}/{host}.svg returns an SVG badge with the game name and the server's player count, or offline. Add :port after the host for a non-default game port (bracket IPv6 addresses, as in [2001:db8::1]:27015) and ?queryPort= to override the query port. The Copy badge button in a query's JSON view writes this Markdown for you.

Embed a badgeMarkdown
[![Minecraft server status](https://query.host/api/v1/badge/minecraft/mc.example.com.svg)](https://query.host/minecraft-java/mc.example.com)

Badges are cached for 60 seconds while online and 30 seconds while offline, and every viewer of a server shares one cached query. Badge views do not count toward your per-IP query limit; uncached badges share their own budget, and when it is spent a badge shows its last known state or unavailable.

Every query has a page at https://query.host/{game}/{host}, with the same :port and ?queryPort= rules as badges. Opening it runs the query, and link previews in chat apps and social sites show a card with the server's name and status, drawn from the badge's cached query. The link button next to a result copies its page URL.

Limits

  • 8 queries per IP address each minute.
  • 60 queries per minute across all public API callers.
  • Results are cached for up to 10 seconds, so polling faster than that returns the same data.
  • Request bodies are capped at 2 KiB.

Past a limit the API returns 429 with a Retry-After header. The playground on query.host has its own budget, so API traffic never blocks it.

HTTP errors

Request failures return { error: { code, message } }. Codes are BAD_REQUEST, BODY_TOO_LARGE, METHOD_NOT_ALLOWED, NOT_FOUND, RATE_LIMITED (your IP or the API budget), OVERLOADED (the query service is at capacity), UPSTREAM_INVALID, and UPSTREAM_UNAVAILABLE.

Browser use

Every route sends Access-Control-Allow-Origin: *, so web apps on any site can call the API directly. Requests never carry cookies or credentials.

Versioning

v1 only changes in compatible ways: new games, new optional result fields, and new error codes. Treat unknown fields and codes as optional. A breaking change will ship as v2 alongside v1.