Ethyrial Public API

Read-only JSON over HTTPS. No key, no signup: every endpoint below is public.

Markets (public, no key)

Trading post reads. Every response is a JSON object; list endpoints carry page, pageSize, total and rows.

EndpointReturns
GET /api/markets A usage document: the resource vocabulary below, so a caller who guesses the base path gets a map instead of a 404.
GET /api/markets/catalog Every market-tracked item with live price, daily change and a price sparkline, plus the category counts for the whole set. Params page, pageSize (default 24), q (min 2 chars), category, sort = name|price|change|supply|demand, dir = asc|desc.
GET /api/markets/items Item search for pickers. Params q (min 2 chars), limit.
GET /api/markets/orders The live order book for one item, fully-filled orders excluded. Sell side cheapest first, buy side highest bid first. Params itemId (required), side = sell|buy (required), page, pageSize.
GET /api/markets/trades The executed trade ledger for one item, newest first. Params itemId (required), page, pageSize.
GET /api/markets/bars Price bars, oldest to newest. Params itemId (required), interval = daily|weekly (required), take.
GET /api/markets/summary/{itemId} One item's market snapshot: current and previous price, daily change, min and max, supply and demand depth, last trade. An item with no market data answers 404 {"error": "item not found"}.
GET /api/markets/index The whole tracked name list in one cacheable answer, for pickers and deep links that must not page the catalogue. Takes no parameters. rows is an array of compact tuples [itemId, name, category, wikiSlug, presetName] sorted by display name (wikiSlug and presetName are empty strings when absent), with total and linked (rows that carry a wiki slug) counts and source: "fixture". When no index is available the envelope is {"live": false, "reason": "unavailable", "total": 0, "linked": 0, "rows": []} so a caller falls back to /api/markets/items search.

Site data (public, no key)

The endpoints this website reads for its own rankings and armory surfaces. They take no token today; if that ever changes, this page changes with it.

EndpointReturns
GET /api/leaderboards The rankings snapshot: every board with its categories, metadata and current standings, plus the compute timestamp.
GET /api/rank-history One character's rank and progression on one board, read from every retained snapshot. Params name (required), category (default Combat Level), limit (default 120, max 365). Every point carries the snapshotId and the snapshot's own computedAt, so a series is always traceable to the ingest it came from. Without a D1 binding, or for a name no snapshot carries, the envelope is {"live": false, "reason": "no-d1"|"no-history", "points": []}.
GET /api/armory Without ?name=, the character roster this site can answer for. With ?name=<character>, one character's public sheet: level, vocation, playtime, guild standing, tracked stats and per-slot equipment. An unknown name answers 404 {"error": "Character not found.", "code": "CHARACTER_NOT_FOUND"}.
GET /api/freshness When the game-data snapshot behind these surfaces was generated: generatedAt, a content hash and the source revision. Every page's freshness line reads this. Also answers server (the live cluster stamp, null when unaskable) and parity (match/mismatch/unknown against the pipeline revision).
GET /api/comments?entity=&lt;wiki path&gt; The comment thread for one wiki entity, threaded one level. A removed comment returns with deleted: true and an empty body. The response carries no author identifier, only a display name. Writing happens on the account surface, not here.
GET /api/guides?slug=&lt;slug&gt; The published player guides, or one guide with its rendered body when slug is given. Bodies are rendered from a small markdown subset after escaping; links carry rel="nofollow ugc noopener". No author identifier is returned, and a hidden guide answers 404.
GET /api/builds?id=&lt;id&gt; The published player builds, or one build including its planner payload when id is given. The list never carries a payload. No author identifier is returned, and a hidden build answers 404.

Example

curl "https://ethyrial-web.ryan-fec.workers.dev/api/markets/orders?itemId=213&side=sell&page=1&pageSize=2"

{
  "itemId": 213, "side": "sell", "page": 1, "pageSize": 2, "total": 4,
  "rows": [
    { "orderId": 51195, "itemName": "Barbed Arrows", "side": "sell",
      "pricePerUnit": 231, "unitsTotal": 220, "unitsRemaining": 220,
      "sellerName": "Seedmage", "orderTime": "2026-08-12T04:37:07.000Z" },
    { "orderId": 51201, "itemName": "Barbed Arrows", "side": "sell",
      "pricePerUnit": 233, "unitsTotal": 226, "unitsRemaining": 226,
      "sellerName": "Seedmage", "orderTime": "2026-08-25T13:19:16.000Z" }
  ]
}

sellerName is null when the listing character was since deleted; the row still shows. dailyChangePct is null for items without a previous price, and spark is empty until an item has two or more daily bars.

Conventions

Caching and availability

These endpoints are cached and they tell you how old their data is. A degraded answer says so in its headers rather than quietly looking healthy.

Versioning and limits

Every endpoint states which version of the contract answered, and the one path that costs an upstream read tells a caller when to come back.

Webhooks (outbound)

Register an https endpoint on your account and we POST a signed JSON envelope when a selected event happens. The signing secret is shown once.