Resources
All endpoints are GET, unauthenticated, under https://api.liquary.xyz/v1.
Every document carries generatedAt (epoch ms; x-generatedAt in
openapi.json). See Freshness.
What each one weighs
The two aggregates grow with the archive, so read their sizes as a lower bound; everything else is bounded.
| Resource | Typical | Largest seen |
|---|---|---|
live.json | 80 KB | grows with the number of listed markets |
manifest.json | 1 KB | n/a |
markets/{id}.json | 1.5 KB median | 3.9 KB |
markets/{id}/price.json | 9 KB | n/a |
governance.json | 10 KB | n/a |
changes.json | 44 KB | bounded at 500 entries |
openapi.json | 62 KB | n/a |
archive.json | 375 KB | grows with the archive |
events.json | 885 KB | grows with the archive |
markets/{id}/settlement.json | 2 KB | n/a |
manifest.json
The archive’s table of contents. Also served at the API root (/), and at
/v1 and /v1/.
{
"generatedAt": 1788624206161,
"buildSha": "unknown",
"freshness": "regenerated on market settlement (~1 min) and nightly; not continuous",
"license": {
"name": "CC BY 4.0",
"url": "https://creativecommons.org/licenses/by/4.0/",
"attribution": "Liquary (liquary.xyz)"
},
"coverage": { "firstExpiryMs": 1777788000000, "lastExpiryMs": 1788624000000 },
"counts": { "settledMarkets": 866, "settledEvents": 601, "marketFiles": 1467 },
"shards": [{ "month": "2026-05", "markets": 123, "events": 1 }, "…"],
"resources": ["/v1/archive.json", "/v1/events.json", "…"],
"window": {
"days": null, "from": null, "basis": "settlement",
"note": "No window: every settled market the archive holds is served, back to May 2026, …"
}
}resources lists the entry points. price.json and settlement.json are not
listed: reach them through a market document’s priceUrl and settlementUrl.
shards lists every month the archive holds, May 2026 onward. window is kept
for compatibility and always reads days: null: there is no boundary.
live.json
Every market Hyperliquid currently lists, named. This is the one document here that is not a settled record.
Hyperliquid publishes a permissionless market as a template pointer plus
parameters (name: "template:sportsContestWinner", description: "participantA:Tampa Bay Buccaneers|participantB:Minnesota Vikings|competition:NFL|…").
live.json resolves that into the name a reader sees. Read it by outcomeId
(or by a leg’s outcomeId inside legs).
Identity and structure only. No prices: read Hyperliquid’s allMids by
yesCoin / noCoin. No volume, no history: a market that settles leaves
this document and appears in archive.json or events.json. Rebuilt every
60 seconds; a newly listed market appears within a few minutes. Ordered
ascending by id (outcomeId for a binary, questionId for a question, the two
id spaces sorted together), so two reads diff cleanly. Answers 503 with
Retry-After: 60, never an empty list, when Hyperliquid did not answer.
{
"generatedAt": 1790081859199,
"note": "Every HIP-4 market Hyperliquid currently lists, with its template pointer resolved into the name a reader sees. …",
"license": { "name": "CC BY 4.0", "url": "…", "attribution": "Liquary (liquary.xyz)" },
"count": 136,
"markets": [
{
"kind": "binary",
"outcomeId": 4593,
"questionId": null,
"title": "NFL: Tampa Bay Buccaneers vs Minnesota Vikings",
"rawName": "template:sportsContestWinner",
"template": "sportsContestWinner",
"sides": ["TB", "MIN"],
"yesCoin": "#45930",
"noCoin": "#45931",
"legs": null,
"category": "sport",
"competition": "NFL",
"fixture": {
"home": "Tampa Bay Buccaneers", "away": "Minnesota Vikings",
"competition": "NFL", "sport": "Football",
"startMs": 1790539500000, "stage": "Regular Season"
},
"underlying": null,
"expiryMs": 1790625900000,
"scalar": null,
"deployer": { "protocol": false, "venue": "out", "address": "0x0c46eb73fae2816f219fcf11f50d6d3c59b5819e" },
"url": "https://liquary.xyz/predictions/4593"
},
{
"kind": "question",
"outcomeId": null,
"questionId": 315,
"title": "UEFA Nations League: Netherlands vs Germany",
"rawName": "template:sportsContestResult",
"template": "sportsContestResult",
"sides": null, "yesCoin": null, "noCoin": null,
"legs": [
{ "outcomeId": 4295, "label": "Netherlands", "yesCoin": "#42950", "noCoin": "#42951" },
{ "outcomeId": 4296, "label": "Draw", "yesCoin": "#42960", "noCoin": "#42961" },
{ "outcomeId": 4297, "label": "Germany", "yesCoin": "#42970", "noCoin": "#42971" }
],
"category": "sport",
"competition": "UEFA Nations League",
"fixture": { "home": "Netherlands", "away": "Germany", "competition": "UEFA Nations League", "sport": "Soccer", "startMs": 1790275500000, "stage": "League A, Matchday 1" },
"underlying": null, "expiryMs": 1790361900000, "scalar": null,
"deployer": { "protocol": false, "venue": "out", "address": "0x0c46eb73fae2816f219fcf11f50d6d3c59b5819e" },
"url": "https://liquary.xyz/predictions/family/uefa-nations-league-netherlands-vs-germany-4295"
}
]
}| Field | Meaning |
|---|---|
kind | binary = one outcome with two sides. question = several named legs (a 3-way match result, a price ladder, a policy decision), each leg its own outcome |
outcomeId | HIP-4 outcome id of a binary. null on a question: its ids are in legs |
questionId | Hyperliquid’s question id. null on a binary |
title | The resolved, human-readable name. Never a template: pointer. Times inside a title are UTC for every reader |
rawName | Hyperliquid’s own name: a template:… pointer on a permissionless listing, prose on a validator-era one |
template | The template kind the market was deployed from (sportsContestWinner, sportsContestResult, priceTouch, binaryPrice, scalarPrice, policyRateDecision, …), or null on a validator-era listing. A pointer, not an identity: unrelated markets share priceTouch. Never group on it |
sides | The two side labels of a binary, in Hyperliquid’s side order (side 0, side 1), as the deployer published them: short codes on a fixture (["TB", "MIN"], full names in fixture), ["Yes", "No"] on a plain yes/no binary, ["Long", "Short"] on a scalar. null on a question |
yesCoin / noCoin | The HIP-1 coin tickers of side 0 and side 1 (#N0 / #N1). Key allMids with them for the price. null on a question: see legs |
legs | The named legs of a question, in display order, each with its own outcomeId, label and coin pair. null on a binary |
category | Liquary’s board category: crypto, stocks, commodities, macro, sport, esport or other |
competition | The competition the market names (NFL, English Premier League), normalised to one spelling per league. null when none |
fixture | Who plays whom, when the template declares a two-sided contest: home, away, competition, sport (deployer free text), startMs (scheduled kickoff, epoch ms UTC; expiryMs is the resolution deadline, hours later), stage. null otherwise |
underlying | The underlying perp of a price market (BTC, xyz:WTIOIL), or null |
expiryMs | Resolution deadline, epoch ms UTC. null when the listing carries none |
scalar | { low, high } on a SCALAR market: the side price is then a level inside that band, not a probability. null on every ordinary binary |
deployer | protocol: true when Hyperliquid listed it itself. Otherwise the venue string the deployer registered (deployer free text, served as written) and its address when Hyperliquid’s deployers list declares the venue |
url | The market’s page on liquary.xyz |
image | The market’s art as an absolute URL, as shown on its card. null when the only art would be the neutral placeholder, so you can draw your own fallback. Not shown in the example above |
Participant names and competition labels are the deployer’s words, and two
deployers can list the same fixture with the sides in opposite order: match on
fixture.home / fixture.away, not on side 0. A template Liquary does not
support yet is absent from this document rather than served with its
pointer as a title, so count can be lower than outcomeMeta’s for a few days
after a new template kind ships.
The envelope
archive.json and events.json wrap their rows under different keys:
// archive.json
{ "generatedAt": 1789523883634, "count": 1823,
"coverage": { "firstExpiryMs": …, "lastExpiryMs": … },
"window": { "days": null, "from": null, "basis": "settlement", "note": "No window: …" },
"settled": [ /* rows */ ] }
// events.json
{ "generatedAt": 1789523883634, "count": 542,
"window": { "days": null, "from": null, "basis": "settlement", "note": "No window: …" },
"events": [ /* rows */ ] }coverage spans the whole archive, price and event markets together. The
monthly shards use the same key as their whole-archive counterpart.
archive.json
Every price market (crypto binaries and buckets) ever settled, newest
first, under settled.
| Field | Meaning |
|---|---|
outcomeId | HIP-4 outcome id (also the key for /v1/markets/{id}.json) |
underlying | BTC, ETH, SOL, HYPE, … |
period | Market cadence, e.g. 1d |
class | priceBinary (above/below target) or priceBucket (range) |
expiryMs | Expiry timestamp (epoch ms). Price markets always have one |
targetPrice | The strike the question was asked about |
settlePrice | The oracle price the market settled against |
result | up (YES won) or down (NO won) |
yesCoin / noCoin | The HIP-4 coin tickers, e.g. #10570 / #10571 |
bucketLabel | Range label for bucket legs, null for binaries |
totalVolume | Lifetime volume in USDC, YES and NO legs summed, protocol actions included. Traded volume is buyNotional + sellNotional; do not subtract (see coverage) |
yesVolume / noVolume | The same dollars, split by which side was bought. null means never captured, not zero (see coverage) |
traders | Distinct wallets that traded this market, both legs unioned. null means not swept yet, not zero |
splitNotional | Everything in totalVolume that is not a taker buy or sell: the settlement payout, merges, negations and minting |
buyNotional / sellNotional | Taker buys and sells, in USD. Their sum is what changed hands. null until the fills sweep has covered the market, never 0 |
settleNotional | The settlement payout alone (winners redeemed at $1), inside splitNotional |
tailNotional | The post-settle tail: dollars traded after settlement. Already included in totalVolume; subtract it to compare with trackers that cut at settlement. null until the fills sweep has covered the market |
familyTraders | Distinct wallets across every leg of the market’s family (bucket ladder), as a set union, not the sum of each leg’s traders. Identical on every row of the same family; null on standalone markets |
events.json
Every event market (macro decisions, sports fixtures, one-off questions)
ever resolved, under events, with the winning side and, for matches, the
final score.
| Field | Meaning |
|---|---|
outcomeId | HIP-4 outcome id |
outcomeLabel | The outcome’s own label, e.g. No change. Distinct from questionTitle, which names the parent question |
yesCoin / noCoin | The HIP-4 coin tickers of the two legs, e.g. #8130 / #8131 |
side0 / side1 | The two sides as listed on-chain (usually Yes / No) |
winnerSide | Index of the winning side: 0, 1, or -1, meaning nobody won (a drawn match pays both sides $0.50; winnerLabel reads Draw). Handle -1 explicitly: winnerSide === 0 ? side0 : side1 would crown the wrong side |
winnerLabel | The winning side’s label, resolved for you |
expiryMs | Scheduled expiry (epoch ms). null unless the market published a deadline distinct from the moment it resolved (an outright such as 2026 World Cup Champion has none). Date and sort on settledAt |
settledAt | When the market resolved (epoch ms). Always present |
questionTitle | The parent question, e.g. July Fed funds decision. null on standalone markets, where outcomeLabel is the market’s own name |
questionDescription | The full resolution rules, verbatim. null when the market resolved before we captured it, or when it never had any |
category | e.g. economics/N/A, sports/football: the metadata tag Hyperliquid publishes in a market’s description. A market listed without a tag takes the value of its siblings in the same batch |
totalVolume | Lifetime volume in USDC, YES and NO legs summed, protocol actions included. Traded volume is buyNotional + sellNotional; do not subtract |
yesVolume / noVolume | The same dollars, split by which side was bought. null means never captured, not zero (see coverage) |
traders | Distinct wallets that traded this market, both legs unioned. null means not swept yet, not zero |
familyTraders | Distinct wallets across every outcome of the same parent question, as a set union, not the sum. Identical on every row sharing a questionTitle; null on standalone markets |
splitNotional | Everything in totalVolume that is not a taker buy or sell: the settlement payout, merges, negations and minting |
buyNotional / sellNotional | Taker buys and sells, in USD. Their sum is what changed hands. null until the fills sweep has covered the market, never 0 |
settleNotional | The settlement payout alone (winners redeemed at $1), inside splitNotional |
tailNotional | The post-settle tail: dollars traded after settlement. Already included in totalVolume; subtract it to compare with trackers that cut at settlement. null until the fills sweep has covered the market |
homeTeam / awayTeam / homeScore / awayScore | Final score for sports fixtures, null otherwise |
Every field above is present on every event row: optional ones carry null
rather than disappearing. Three more fields appear only on the rows that have
them, and are absent elsewhere:
| Field | Meaning |
|---|---|
description | The leg’s own parameters, as Hyperliquid published them. On a permissionless leg, whose name is a pointer, this is where the participant’s name can be read |
homeLogo / awayLogo | The two teams’ crests, captured with the final score |
archive/{YYYY-MM}.json and events/{YYYY-MM}.json
The same rows, sliced by the month a market resolved in. The manifest’s
shards array lists every month that exists, with its counts; every listed
month is served, and 404 is a month the archive never had.
curl https://api.liquary.xyz/v1/archive/2026-09.jsonchanges.json
The most recent resolutions, newest first: outcomeId, type, settledAt
and the market’s URL. Poll this instead of re-reading the aggregates.
Bounded at 500 entries. If your last sync is older than oldestSettledAt, the
feed cannot tell you what you missed (its note field says so): re-read
archive.json and events.json.
openapi.json
An OpenAPI 3.1 description of this surface, generated with the live counts
and coverage. Every response is described field by field, and the two row
shapes are named schemas (PriceMarket, EventMarket), so generated clients
are typed.
npx @openapitools/openapi-generator-cli generate \
-i https://api.liquary.xyz/v1/openapi.json -g typescript-fetch -o ./clientgovernance.json
Aggregated validator settlement activity: settlement actions observed, vote counts, and the most active validators by address.
| Field | Meaning |
|---|---|
counts.byType | One entry per action type (register, settle) with its count |
counts.actions / finalized | Totals over all observed actions |
counts.votes / validators | Total votes recorded, and how many distinct validators cast them |
counts.settlementsCovered / settlementsTotal | The share of the validator-settled record the other figures rest on, in markets: settlementsTotal counts only the markets a validator vote settles, settlementsCovered those whose votes we hold |
counts.settlementsDeployerSettled / settlementsPriceSettled | Markets resolved by their third-party deployer, and price markets resolved from the oracle. Neither opens a validator vote, so both are kept out of settlementsTotal |
counts.lastActionAt | When the newest governance action was observed. Not a heartbeat: it stays quiet whenever no action occurs. Judge freshness from the settlement dates in changes.json |
topValidators | The most active validators: address, actions voted on, and their own medianLagMs / lagSamples / lagBounded / openingBeatVotes |
voteLag | How long validators take to vote, measured from the action’s opening (see below) |
recent | The 30 latest actions: id, type, title, reason, quorumReached, finalized (0/1), lastSeen |
voteLag
Lags are measured from the action’s opening, which Hyperliquid does not
publish: it is derived as expireTime - votingWindowMs.
| Field | Meaning |
|---|---|
medianMs | Median lag across every measured vote. null below minSamples |
samples / bounded | Measured votes behind the median, and votes excluded from it because they are upper bounds rather than measurements |
lagResolutionMs | Sampling interval. Two votes within one interval cannot be ordered |
basis | Where the zero sits (actionOpening) |
votingWindowMs | The assumed window between opening and expiry |
windowViolations | Actions that outlived that window. Must be 0: anything else means the window changed and every lag here is off by the same amount |
minSamples | No median is published below this many measured votes |
n, quorum_reached and last_seen are deprecated aliases of count,
quorumReached and lastSeen. Read the camelCase ones; a future major version
drops the others.
markets/{id}.json
One market in full. A market still trading has no document until it settles
(read it in live.json).
| Field | Meaning |
|---|---|
type | price or event |
market | The market’s row, exactly as in archive.json / events.json |
dataQuality | The document’s own report card: { complete, missing }. missing lists reason slugs: odds-none (no curve at all), price-series, volume-split, settlement-votes, fills. complete is simply missing.length === 0. It improves as holes are filled: re-read a market you cached while it was incomplete |
deployer | Who deployed the market: { address, name, protocol }. Hyperliquid’s own markets carry address: null, name: "Hyperliquid", protocol: true; third-party markets carry the deployer address and, when our registry names it, a display name (name is null otherwise). Older documents lack the field; there, absence means protocol |
family | The other legs of the same question: totalVolume across all of them and outcomeIds listing every one. null when the market stands alone |
priceMeta | Summary of what the underlying did while the market was live, at 5-minute resolution: { interval, points, firstTs, lastTs }. null on event markets, which have no ticker, and on markets whose window predates this capture |
priceUrl | Path to that series, or null in the same two cases |
oddsMeta | Summary of the odds series: points, firstTs, lastTs, gaps (a count, not a list), maxGapMs, complete, captureRegime and source (see coverage) |
oddsUrl | Always null: the odds series is not served. oddsMeta describes what was captured |
tradeOddsMeta | Summary of the trade odds: every real transaction price (YES coin, buys and sells only, consecutive identical prices collapsed). A different instrument from oddsMeta, never blended with it, and present even where no mid was captured. null = no real trades |
tradeOddsUrl | Always null, like oddsUrl |
flow | Fills-derived aggregates: traders, fees, notionals, the post-settle tail and derived stats (see below). Carries a basis saying which record it comes from; null only when no fill of the market is known |
settlement | The settlement action: reason, quorumReached, finalized, the timestamps, and voterCount. Never the roster itself (see settlementUrl). null when none was captured |
settlementUrl | Path to the voter roster (/v1/markets/{id}/settlement.json), or null. It already starts with /v1: prefix it with the host, not with the base URL |
settlementApplicable | Whether this market settles by validator vote at all. false on price markets (settled from the oracle) and third-party markets (resolved by their deployer): neither is a gap (see coverage) |
settlementCaptured | false means the votes were never captured, not that nobody voted. Only meaningful when settlementApplicable is true |
curl https://api.liquary.xyz/v1/markets/813.json{
"generatedAt": 1788624206161,
"type": "event",
"dataQuality": { "complete": true, "missing": [] },
"deployer": { "address": null, "name": "Hyperliquid", "protocol": true },
"market": {
"outcomeId": 813,
"questionTitle": "World Cup Semifinal: France vs Spain",
"side0": "France", "side1": "Spain",
"winnerSide": 1, "winnerLabel": "Spain",
"yesCoin": "#8130", "noCoin": "#8131",
"homeTeam": "France", "homeScore": 0, "awayTeam": "Spain", "awayScore": 2,
"expiryMs": null, "settledAt": 1784055600000,
"totalVolume": 8121997,
"yesVolume": 2301277.9925200003, "noVolume": 5820718.570710001,
"tailNotional": 4927922.262750001,
"traders": 1399
},
"family": null,
"priceMeta": null,
"priceUrl": null,
"oddsMeta": {
"points": 5987, "firstTs": 1783754570921, "lastTs": 1784071248507,
"gaps": 65, "maxGapMs": 6587168,
"complete": true, "captureRegime": "event-5s", "source": "mixed"
},
"oddsUrl": null,
"tradeOddsMeta": { "points": 8189, "firstTs": 1783755987011, "lastTs": 1784070782196 },
"tradeOddsUrl": null,
"settlement": {
"found": true,
"reason": "FIFA officially declared Spain the winner of the Game.",
"quorumReached": true, "finalized": true,
"expireTime": 1784668999635,
"firstSeen": 1784064279788, "lastSeen": 1784669329013,
"voterCount": 14
},
"settlementUrl": "/v1/markets/813/settlement.json",
"settlementApplicable": true,
"settlementCaptured": true
}flow is omitted here: it has its own section.
The flow object
Every figure is deduplicated per trade: the chain reports one row per participant, two per trade.
{
"fills": 22332,
"tradeFills": 19548,
"traders": 1399,
"fee": 108.56457842000022,
"builderFee": 108.56457842000022,
"buyNotional": 2640394.304430007,
"sellNotional": 2160517.2587999944,
"splitNotional": 3321085,
"settleNotional": 2947414,
"tailNotional": 4927922.262750001,
"tailFills": 7964,
"tailTradeFills": 6548,
"vwapYes": 0.4170596725208486,
"vwapNo": 0.55919455713321,
"biggestFill": 65758.05428,
"bestEntryPx": 0.38,
"deduplicated": true
}Market #813, unrounded: values arrive at full float precision, not formatted
for display. Current documents also carry tradeFee, tradeBuilderFee and
basis, described below.
| Field | Meaning |
|---|---|
basis | Which record the object comes from. ledger: the settlement sweep has recomputed the market, and every field is populated. live: our own fills record, before the sweep; tradeFills, tailTradeFills, vwapYes, vwapNo, biggestFill and bestEntryPx are then null, and the tail figures read 0 until the sweep. estimate: no fill rows held yet, only the buy and sell notionals seen on the live trade feed; the counts and fees then read 0, which means unknown, not nothing |
fills | Every distinct on-chain row across both legs, deduplicated by trade id. Not all of them are trades (merges, settlements and mints are rows too): read tradeFills if you mean trades |
tradeFills | Buys and sells only: somebody chose a price and somebody took it. null until the recompute reaches the market, never 0 |
traders | Distinct wallet addresses across both legs |
fee / builderFee | USD, and fee includes builderFee. Valued at each fill’s own price where the chain charges in outcome-token shares rather than USDC. Counted on protocol actions (merges, settlements) as well as order-book trades. builderFee is the part routed through builder codes |
tradeFee / tradeBuilderFee | The same two figures on buys and sells only, without the fees charged on protocol actions. null, never 0, while any of the market’s days predates this split; tradeBuilderFee is also null on the live basis |
buyNotional / sellNotional | Notional of taker-buy / taker-sell trades, USDC. Their sum is the order-book volume |
splitNotional | Everything that is not a taker buy or sell: settlement (also served alone as settleNotional), merges, negations and minting. For an order-book-only figure, add buyNotional + sellNotional rather than subtracting this from a total (see coverage) |
tailNotional / tailFills | The post-settle tail: dollars and rows moving after settlement, the protocol’s own settlement rows included. Already included in the totals above; subtract it to compare with trackers that cut at settlement |
tailTradeFills | The tradeFills (real trades, not rows) that happened after settlement: the late-trading count. null until recomputed |
settleNotional | The settlement payout alone (winners redeemed at $1), in USD, part of splitNotional. buyNotional + sellNotional is the trading, + settleNotional is trading plus the payout; merges, negations and mints are in neither. null until the recompute reaches the market, never 0 |
deduplicated | true = per-trade figures with USD fees. false = an early sweep row not yet recomputed; treat its notional as inflated |
vwapYes / vwapNo | Volume-weighted average price per coin across its real trades, splits excluded. null until derived |
biggestFill | Largest single fill by notional, USD. null until derived |
bestEntryPx | Cheapest real buy price of the winning side. Fills under $10 are ignored as dust. Only derivable once the winner is known |
markets/{id}/settlement.json
Which validators voted the settlement, with the name and stake weight each
carried at vote time. Fetch it when settlementUrl is non-null. The file
also repeats the action’s expireTime, firstSeen and lastSeen.
{
"generatedAt": 1787356995910,
"outcomeId": 674,
"found": true,
"reason": "FIFA officially declared France the winner of the Game.",
"quorumReached": true,
"finalized": true,
"voters": [
{ "validator": "0x0000…acb8", "name": "ValiDAO", "stakePct": 1.5, "firstSeen": 1784160139953, "source": null }
]
}| Voter field | Meaning |
|---|---|
validator | The validator’s address |
name / stakePct | Its name and its share of the stake, snapshotted when the vote was captured. null on votes indexed before these were recorded |
firstSeen | When we first saw the vote (epoch ms) |
source | null when we witnessed the vote while the action was in flight; "chain" when the voter was rebuilt afterwards from the L1 vote transaction |
curl https://api.liquary.xyz/v1/markets/674/settlement.jsonmarkets/{id}/price.json
What the underlying did over the window the market was live, at 5-minute resolution: Hyperliquid’s own price, the one the market settled against. Frozen at settlement.
{
"generatedAt": 1787330633626,
"outcomeId": 3,
"interval": "5m",
"points": [ { "t": 1777960800000, "c": 1823.4 }, "…" ]
}Fetch it when priceUrl is non-null. It is null on event markets (no
ticker) and on the few price markets that closed before this capture existed.
curl https://api.liquary.xyz/v1/markets/3/price.jsonStatus codes
404: an id or a path the archive does not serve. That includes a market still trading, the odds series (odds.json,trade-odds.json), withdrawn market #16 (see coverage), and anything outside/v1, which answers a JSON404(/robots.txtaside). A404is cached for up to five minutes (see Freshness).410: the bulk export (/v1/bulk/…), withdrawn.429: over 300 requests per minute per IP. The body,Retry-After: 60andX-RateLimit-Limitsay so.403: about the caller, not the resource: every path answers the same way. It is never about authentication, and it is not the rate limiter.
A 403 is most often your User-Agent. A client that sends no
User-Agent, or the default one from Python’s urllib, is refused on its
first request. Send any real identifier:
# 403 on request #1
urllib.request.urlopen("https://api.liquary.xyz/v1/manifest.json")
# 200: requests sets its own User-Agent, and any custom one works too
requests.get("https://api.liquary.xyz/v1/manifest.json",
headers={"User-Agent": "my-app/1.0 (contact@example.com)"})