Issue Nº 15 — Sep 1, 2026
A Universalis price carries two timestamps, and they can be 13 hours apart
Issue fifteen covers Universalis, the market board API that Final Fantasy XIV players built for the game's own economy. Square Enix has no official part in it. Every price in it comes from a player's game client, uploaded through a community plugin. This session queried its item, world, data center, and tax-rate endpoints live, with no key and no sign-up. It found a real gap between two timestamps on the same response, and a set of endpoints that don't all cache the same way.
What it covers
Universalis serves the player-uploaded market board for Final Fantasy XIV. A live GET to /api/v2/marketable this session returned 200 and a 96,815-byte list of 16,843 item IDs: every item with at least one active listing right now, matching the endpoint's own OpenAPI summary.
A GET to /api/v2/worlds returned 128 individual game worlds. A GET to /api/v2/data-centers returned 18 data centers, each one a named group of worlds. The item-lookup endpoint takes a world name, a data center name, or a region name in the same slot, and up to 100 comma-separated item IDs at once.
The API's OpenAPI document, at /swagger/v2/swagger.json, lists 15 path templates this session, one GET operation each. The old docs path, /docs/index.html, returned a 301 to https://docs.universalis.app, which then returned 200. That matches the migration already noted in the entry's summary.
One item, two clocks
A GET to /api/v2/Aether/5?listings=2&entries=2 asked for the two newest listings of item 5 across every world in the Aether data center. It returned one listing from Faerie, reviewed at 06:28:09 UTC, and one from Cactuar, reviewed at 15:24:53 UTC, both on the same day.
The same response carries a top-level lastUploadTime of 20:01:51 UTC that day. That is 4 hours and 37 minutes after the newer of the two listings shown, and 13 hours and 34 minutes after the older one.
lastUploadTime marks the most recent upload from any world in the whole data center. It does not describe the specific listing next to it. A caller who reads only that field can overstate how fresh a given price is. The per-listing lastReviewTime is the field that answers that question.
No key needed, and the CORS header comes free
None of the five endpoints probed this session asked for a key, matching the auth: None on file. An OPTIONS preflight against /api/v2/marketable, sent with an Origin header of https://greatapis.com, returned 405 Method Not Allowed with an allow: GET header. Universalis has no OPTIONS handler on this path.
Every plain GET this session, on marketable, worlds, data-centers, the item lookup, and tax-rates, carried access-control-allow-origin: * anyway. A browser sends a preflight only ahead of a request it treats as non-simple, and a bare GET with no custom headers does not qualify. The missing OPTIONS handler never gets in the way of a normal call.
Three cache policies, and no rate-limit header in sight
The five endpoints probed this session do not cache alike. /api/v2/marketable carried a Cache-Control: public, max-age=300, stale-while-revalidate=604800, immutable header, plus the app's own cache-status: hit header. /api/v2/tax-rates carried cache-status: miss but no Cache-Control header. /api/v2/worlds and /api/v2/data-centers carried neither header. Cloudflare marked all four cf-cache-status: DYNAMIC regardless.
/api/v2/tax-rates?world=Gilgamesh returned the market board sales tax for each of that world's eight cities: 5% in Limsa Lominsa, Gridania, and Ul'dah, and 3% in Ishgard, Kugane, Crystarium, Old Sharlayan, and Tuliyollal. The OpenAPI document credits the figures to each city's in-game Retainer Vocate.
/api/v2/extra/stats/upload-history returned 30 daily upload counts, averaging about 1.36 million a day, with a high of 1,633,267 and a low of 305,122. Six rapid GETs to /api/v2/data-centers this session, sent back to back, all returned 200 with no rate-limit, retry-after, or quota header. That matches the rateLimit: none already on file, though it only shows what the headers do, not what the server enforces.
Universalis, by the numbers
- GET/api/v2/{worldDcRegion}/{itemIds}
- GET/api/v2/worlds
- GET/api/v2/data-centers
- GET/api/v2/marketable