APIHIP-4 ArchiveResources

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.

ResourceTypicalLargest seen
live.json80 KBgrows with the number of listed markets
manifest.json1 KBn/a
markets/{id}.json1.5 KB median3.9 KB
markets/{id}/price.json9 KBn/a
governance.json10 KBn/a
changes.json44 KBbounded at 500 entries
openapi.json62 KBn/a
archive.json375 KBgrows with the archive
events.json885 KBgrows with the archive
markets/{id}/settlement.json2 KBn/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"
    }
  ]
}
FieldMeaning
kindbinary = 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
outcomeIdHIP-4 outcome id of a binary. null on a question: its ids are in legs
questionIdHyperliquid’s question id. null on a binary
titleThe resolved, human-readable name. Never a template: pointer. Times inside a title are UTC for every reader
rawNameHyperliquid’s own name: a template:… pointer on a permissionless listing, prose on a validator-era one
templateThe 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
sidesThe 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 / noCoinThe 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
legsThe named legs of a question, in display order, each with its own outcomeId, label and coin pair. null on a binary
categoryLiquary’s board category: crypto, stocks, commodities, macro, sport, esport or other
competitionThe competition the market names (NFL, English Premier League), normalised to one spelling per league. null when none
fixtureWho 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
underlyingThe underlying perp of a price market (BTC, xyz:WTIOIL), or null
expiryMsResolution 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
deployerprotocol: 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
urlThe market’s page on liquary.xyz
imageThe 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.

FieldMeaning
outcomeIdHIP-4 outcome id (also the key for /v1/markets/{id}.json)
underlyingBTC, ETH, SOL, HYPE, …
periodMarket cadence, e.g. 1d
classpriceBinary (above/below target) or priceBucket (range)
expiryMsExpiry timestamp (epoch ms). Price markets always have one
targetPriceThe strike the question was asked about
settlePriceThe oracle price the market settled against
resultup (YES won) or down (NO won)
yesCoin / noCoinThe HIP-4 coin tickers, e.g. #10570 / #10571
bucketLabelRange label for bucket legs, null for binaries
totalVolumeLifetime volume in USDC, YES and NO legs summed, protocol actions included. Traded volume is buyNotional + sellNotional; do not subtract (see coverage)
yesVolume / noVolumeThe same dollars, split by which side was bought. null means never captured, not zero (see coverage)
tradersDistinct wallets that traded this market, both legs unioned. null means not swept yet, not zero
splitNotionalEverything in totalVolume that is not a taker buy or sell: the settlement payout, merges, negations and minting
buyNotional / sellNotionalTaker buys and sells, in USD. Their sum is what changed hands. null until the fills sweep has covered the market, never 0
settleNotionalThe settlement payout alone (winners redeemed at $1), inside splitNotional
tailNotionalThe 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
familyTradersDistinct 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.

FieldMeaning
outcomeIdHIP-4 outcome id
outcomeLabelThe outcome’s own label, e.g. No change. Distinct from questionTitle, which names the parent question
yesCoin / noCoinThe HIP-4 coin tickers of the two legs, e.g. #8130 / #8131
side0 / side1The two sides as listed on-chain (usually Yes / No)
winnerSideIndex 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
winnerLabelThe winning side’s label, resolved for you
expiryMsScheduled 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
settledAtWhen the market resolved (epoch ms). Always present
questionTitleThe parent question, e.g. July Fed funds decision. null on standalone markets, where outcomeLabel is the market’s own name
questionDescriptionThe full resolution rules, verbatim. null when the market resolved before we captured it, or when it never had any
categorye.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
totalVolumeLifetime volume in USDC, YES and NO legs summed, protocol actions included. Traded volume is buyNotional + sellNotional; do not subtract
yesVolume / noVolumeThe same dollars, split by which side was bought. null means never captured, not zero (see coverage)
tradersDistinct wallets that traded this market, both legs unioned. null means not swept yet, not zero
familyTradersDistinct 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
splitNotionalEverything in totalVolume that is not a taker buy or sell: the settlement payout, merges, negations and minting
buyNotional / sellNotionalTaker buys and sells, in USD. Their sum is what changed hands. null until the fills sweep has covered the market, never 0
settleNotionalThe settlement payout alone (winners redeemed at $1), inside splitNotional
tailNotionalThe 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 / awayScoreFinal 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:

FieldMeaning
descriptionThe 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 / awayLogoThe 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.json

changes.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 ./client

governance.json

Aggregated validator settlement activity: settlement actions observed, vote counts, and the most active validators by address.

FieldMeaning
counts.byTypeOne entry per action type (register, settle) with its count
counts.actions / finalizedTotals over all observed actions
counts.votes / validatorsTotal votes recorded, and how many distinct validators cast them
counts.settlementsCovered / settlementsTotalThe 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 / settlementsPriceSettledMarkets 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.lastActionAtWhen 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
topValidatorsThe most active validators: address, actions voted on, and their own medianLagMs / lagSamples / lagBounded / openingBeatVotes
voteLagHow long validators take to vote, measured from the action’s opening (see below)
recentThe 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.

FieldMeaning
medianMsMedian lag across every measured vote. null below minSamples
samples / boundedMeasured votes behind the median, and votes excluded from it because they are upper bounds rather than measurements
lagResolutionMsSampling interval. Two votes within one interval cannot be ordered
basisWhere the zero sits (actionOpening)
votingWindowMsThe assumed window between opening and expiry
windowViolationsActions that outlived that window. Must be 0: anything else means the window changed and every lag here is off by the same amount
minSamplesNo 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).

FieldMeaning
typeprice or event
marketThe market’s row, exactly as in archive.json / events.json
dataQualityThe 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
deployerWho 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
familyThe other legs of the same question: totalVolume across all of them and outcomeIds listing every one. null when the market stands alone
priceMetaSummary 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
priceUrlPath to that series, or null in the same two cases
oddsMetaSummary of the odds series: points, firstTs, lastTs, gaps (a count, not a list), maxGapMs, complete, captureRegime and source (see coverage)
oddsUrlAlways null: the odds series is not served. oddsMeta describes what was captured
tradeOddsMetaSummary 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
tradeOddsUrlAlways null, like oddsUrl
flowFills-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
settlementThe settlement action: reason, quorumReached, finalized, the timestamps, and voterCount. Never the roster itself (see settlementUrl). null when none was captured
settlementUrlPath 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
settlementApplicableWhether 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)
settlementCapturedfalse 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.

FieldMeaning
basisWhich 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
fillsEvery 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
tradeFillsBuys and sells only: somebody chose a price and somebody took it. null until the recompute reaches the market, never 0
tradersDistinct wallet addresses across both legs
fee / builderFeeUSD, 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 / tradeBuilderFeeThe 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 / sellNotionalNotional of taker-buy / taker-sell trades, USDC. Their sum is the order-book volume
splitNotionalEverything 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 / tailFillsThe 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
tailTradeFillsThe tradeFills (real trades, not rows) that happened after settlement: the late-trading count. null until recomputed
settleNotionalThe 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
deduplicatedtrue = per-trade figures with USD fees. false = an early sweep row not yet recomputed; treat its notional as inflated
vwapYes / vwapNoVolume-weighted average price per coin across its real trades, splits excluded. null until derived
biggestFillLargest single fill by notional, USD. null until derived
bestEntryPxCheapest 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 fieldMeaning
validatorThe validator’s address
name / stakePctIts name and its share of the stake, snapshotted when the vote was captured. null on votes indexed before these were recorded
firstSeenWhen we first saw the vote (epoch ms)
sourcenull 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.json

markets/{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.json

Status 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 JSON 404 (/robots.txt aside). A 404 is 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: 60 and X-RateLimit-Limit say 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)"})