RE:NODE

PaperMC download API: scripting Paper, Velocity and Folia jars

By RE:NODE Team
15 min read


If your script still fetches Paper from api.papermc.io/v2/..., it is broken: the v2 API was sunset on 1 July 2026 and every request to it now returns 410 Gone. The replacement is the Fill API at https://fill.papermc.io/v3/, and the change is small once you know it. You ask /v3/projects/paper/versions/<version>/builds?channel=STABLE, take the first build in the list, and the JSON hands you a ready-made download URL on fill-data.papermc.io together with a sha256 checksum. Send a User-Agent that names your script and gives a contact, verify the checksum, and pin the build you tested rather than chasing the newest one. The rest of this post is the detail: every endpoint you need, what the channels mean, a complete update script, and the mistakes that leave a server with a half-downloaded jar.

What happened to api.papermc.io/v2#

For years the download URL for a Paper jar had one well-known shape, and thousands of scripts, Dockerfiles and setup guides hard-coded it:

code
https://api.papermc.io/v2/projects/paper/versions/<ver>/builds/<build>/downloads/<file>.jar

You built it from three pieces - the Minecraft version, the build number and the file name, which was always paper-<ver>-<build>.jar - and you could fetch it without asking the API anything first if you already knew the build. That convenience is gone. PaperMC replaced the old service, called Bibliothek, with a new one called Fill, and retired the v2 endpoints. Today both the metadata endpoints and the old download URLs answer like this:

code
HTTP/1.1 410 Gonesunset: Wed, 1 Jul 2026 00:00:00 GMT{"ok":false,"error":"sunset","message":"This API version has been sunset and isno longer available. To continue using the service, please upgrade to a supportedAPI version."}

There is no redirect and no grace period left. If a server's start script downloads its jar on every boot, or a container image fetches Paper at build time, the symptom is a download that "succeeds" with wget or a plain curl and leaves you with a 176-byte JSON file called server.jar. Java then refuses it with Error: Invalid or corrupt jarfile server.jar, which sends people looking at Java versions instead of at the URL. Using curl -f (fail on HTTP errors) turns this into a clear error at the download step, and it is the first thing the script further down does.

Also note that api.papermc.io/v3 does not exist; the new API lives on a different host, fill.papermc.io, and the jars themselves on a third, fill-data.papermc.io. If your firewall or proxy has an allow list for outbound traffic, all three names have changed.

The Fill v3 endpoints#

The v3 API is a small REST tree with the same shape as before - projects, versions, builds - and JSON throughout. These are the requests a script actually needs:

EndpointReturns
/v3/projectsEvery project with its versions
/v3/projects/paperOne project, versions grouped by family
/v3/projects/paper/versions/26.3Support status, Java minimum, build ids
/v3/projects/paper/versions/26.3/buildsEvery build, newest first, full detail
/v3/projects/paper/versions/26.3/builds?channel=STABLEThe same list, one channel only
/v3/projects/paper/versions/26.3/builds/189One build by number
/v3/projects/paper/versions/26.3/builds/latestThe newest build of any channel

All of them hang off https://fill.papermc.io. The project ids are paper, folia and velocity; the retired waterfall and travertine proxies are still listed for their old builds, but nothing new is published for them.

The project endpoint groups versions by family, newest family first and newest version first within each family:

json
{"project":{"id":"paper","name":"Paper"}, "versions":{"26.3":["26.3","26.3-rc-3"],             "26.2":["26.2","26.2-rc-2"],             "26.1":["26.1.2","26.1.1"],             "1.21":["1.21.11","1.21.11-rc3", "...", "1.21"]}}

Two things to notice. Minecraft's own version numbers changed in 2026 to a year-based scheme, so the list runs from 26.3 back to 1.21.11 and beyond, and anything that sorts versions as strings will put them in the wrong order. And release candidates and pre-releases (26.3-rc-3, 1.21.11-pre5) sit in the same list as real releases. A script that takes "the first version in the list" will one day pick a release candidate. If you want the newest release automatically, filter out ids containing - - though, as below, picking the Minecraft version automatically is a bad idea anyway.

The version endpoint is the one people overlook, and it is the most useful for a careful script:

json
{"version":{"id":"1.21.4",            "support":{"status":"UNSUPPORTED","end":"2025-07-17"},            "java":{"version":{"minimum":21},                    "flags":{"recommended":["-XX:+UseG1GC", "..."]}}}, "builds":[232, 231, "..."]}

support.status tells you whether PaperMC still maintains that version (SUPPORTED or UNSUPPORTED, with an end date), and java.version.minimum tells you which Java the jar needs - 21 for the 1.21 line, 25 for the 26.x releases. A script can refuse to install a jar the machine's Java cannot run, which is far friendlier than the UnsupportedClassVersionError you otherwise get on start. The recommended flags are the familiar G1 set; Minecraft JVM flags and Java versions explains what each of them does.

A build object carries everything needed to download and check the jar:

json
{"id":189, "time":"2026-10-14T20:17:18Z", "channel":"STABLE", "commits":[{"sha":"a48059a2...","message":"Correctly serialise decorated pod ..."}], "downloads":{"server:default":{    "name":"paper-26.3-189.jar",    "checksums":{"sha256":"3a90c2ea79a81cd0ce641c9954c616828be6d32255385aef3b2022f80b91e8b5"},    "size":55150184,    "url":"https://fill-data.papermc.io/v1/objects/3a90c2ea.../paper-26.3-189.jar"}}}

The download key is server:default for all three projects (v2 used application), and the key contains a colon, so in jq it must be quoted: .downloads."server:default".url. The URL itself is content-addressed - the path contains the jar's sha256 - so it never changes for a given build and can be cached safely. Do not build this URL yourself; read it from the API. That is the main behavioural difference from v2, where constructing the URL from version and build was the normal way to work.

Errors come back as JSON with an error code and the right HTTP status: a build that does not exist gives 404 with build_not_found, an unknown version gives version_not_found. The API also offers a GraphQL endpoint at https://fill.papermc.io/graphql for anyone who prefers it; everything in this post uses REST because curl and jq are on every Linux box.

Channels: ALPHA, BETA and STABLE#

v2 had two channels, default and experimental. Fill has three, and every build carries one:

ChannelWhat it means
STABLEThe builds PaperMC recommends for production
BETAFeature-complete for a version, still settling
ALPHAEarly builds of a new Minecraft version

A new Minecraft release starts life as a run of ALPHA builds, moves to BETA once it is usable, and becomes STABLE when PaperMC is happy with it. On 26.3 at the time of writing there were 48 alpha builds, 38 beta builds and 15 stable ones, which gives a sense of how long the unstable stretch is. Release candidates such as 26.3-rc-3 may never get a stable build at all.

PaperMC's own documentation is blunt about this: use stable builds, and do not run unstable builds or auto-updaters in production, because experimental builds are prone to errors and do not receive support. That is worth taking literally. A Discord report that starts with "I'm on an alpha build" gets one answer.

The channel filter on the builds list does accept several values (?channel=STABLE&channel=BETA), which is useful for a staging server that should test betas before they reach production.

User-Agent etiquette, and why it is now a rule#

Under v2, a User-Agent was polite. Under Fill it is required. PaperMC's downloads documentation states that every request must carry a User-Agent that clearly identifies your software or organisation, is not a generic default such as curl/8.5.0 or Wget/1.21, and includes a contact URL or email address. Their own example is the one used by the popular Docker image:

code
mc-image-helper/1.39.11 (https://github.com/itzg/docker-minecraft-server)

A request without one may well work today, especially for responses served from cache - but relying on that is building on something the operators have said they do not allow, and a blocked script fails in exactly the same confusing way as the v2 sunset did. The format that satisfies the rule is name/version (contact):

bash
$ curl -A "smp-updater/1.2 (admin@example.net)" \    https://fill.papermc.io/v3/projects/paper

The rest of the etiquette follows from the same idea: the API is a free service run by volunteers, and a script that hammers it is the reason rules like this exist.

  • Do not poll. Checking once a day, or once before each scheduled restart, is plenty. Paper can publish several builds in one evening; you do not need each of them within minutes.
  • Do not download what you already have. Compare the build id (or the sha256) with the one installed and stop if they match. The script below does this.
  • Respect the cache headers. The metadata responses carry cache-control: max-age=1800. Asking more often than every half hour gets you the same answer.
  • Fetch the jar once per machine, not once per server start. A network that runs eight backends from one machine should download one jar and copy it.

Pinning a build versus following the latest#

There are two quite different things you might mean by "update Paper", and a script should only ever automate one of them.

Builds within one Minecraft version are mostly bug fixes and performance work. Moving from build 187 to 189 of 26.3 does not convert the world or change the plugin API, and a script that follows the latest stable build of a fixed version is reasonable for many servers. Even here, the occasional build changes behaviour a plugin relied on, or tightens something that was being abused - a recent 26.3 build that stopped plugins resetting brewing recipes off the main thread is exactly that kind of change. For a small group server, following stable builds weekly is fine. For a server with dozens of plugins, pin the build you tested and move it forward deliberately.

A new Minecraft version is a different event entirely. The world is converted on first load and cannot be opened by the older version again, plugins compiled against the old version often break, and the clients must match. That should never happen because a script noticed a new entry at the top of the version list. Keep the Minecraft version as a fixed setting, chosen by a person, and walk through Minecraft version upgrades when you change it.

Pinning in practice means writing the build down. Either keep the exact build id in a config file and have the script install that id, or let the script follow stable builds but record each install in a log so you know which build to go back to. The version endpoint lists every build id that exists, so a pinned id stays fetchable. Keep the previous jar beside the current one; rolling back a build within the same Minecraft version is a file rename, provided the world has not been converted.

A curl and jq update script#

This script installs the newest stable build of one fixed Minecraft version, or a pinned build if you give it one. It verifies the checksum before touching the live jar, keeps the previous jar for rollback, and does nothing at all if the installed build is already current. It needs bash, curl, jq and sha256sum.

update-paper.sh
#!/usr/bin/env bash# Usage: update-paper.sh <minecraft-version> [build]set -euo pipefailPROJECT="${PROJECT:-paper}"            # paper, folia or velocityVERSION="${1:?usage: $0 <minecraft-version> [build]}"PIN="${2:-}"                           # optional exact build idDIR="${DIR:-/home/mc/server}"JAR="$DIR/server.jar"STATE="$DIR/.${PROJECT}-build"UA="smp-updater/1.2 (admin@example.net)"API="https://fill.papermc.io/v3/projects/$PROJECT/versions/$VERSION"if [[ -n "$PIN" ]]; then  build=$(curl -fsS -A "$UA" "$API/builds/$PIN")else  build=$(curl -fsS -A "$UA" "$API/builds?channel=STABLE" | jq -e '.[0]') \    || { echo "No stable $PROJECT build for $VERSION" >&2; exit 1; }fiid=$(jq -r '.id' <<<"$build")url=$(jq -r '.downloads."server:default".url' <<<"$build")sha=$(jq -r '.downloads."server:default".checksums.sha256' <<<"$build")name=$(jq -r '.downloads."server:default".name' <<<"$build")if [[ -f "$STATE" && "$(cat "$STATE")" == "$id" ]]; then  echo "$PROJECT $VERSION build $id already installed"; exit 0fitmp=$(mktemp "$DIR/.download.XXXXXX")trap 'rm -f "$tmp"' EXITcurl -fsS -A "$UA" -o "$tmp" "$url"echo "$sha  $tmp" | sha256sum -c --quiet -[[ -f "$JAR" ]] && cp -p "$JAR" "$JAR.prev"mv "$tmp" "$JAR"echo "$id" > "$STATE"echo "$(date -Is) installed $name" >> "$DIR/update.log"

Run it as ./update-paper.sh 1.21.11 to follow stable builds of 1.21.11, or ./update-paper.sh 1.21.11 132 to install exactly build 132. The same script handles Velocity and Folia through the PROJECT variable:

bash
$ PROJECT=velocity DIR=/home/mc/proxy ./update-paper.sh 4.2.0$ PROJECT=folia ./update-paper.sh 26.3

Why each piece is there:

  • curl -f turns an HTTP error into a non-zero exit, so a 410, 404 or blocked request stops the script instead of producing a JSON file named like a jar. -sS keeps the output quiet but still prints real errors.
  • jq -e '.[0]' exits non-zero when the array is empty - a version with no stable builds yet - so the script stops with a message rather than requesting a URL of null.
  • The download goes to a temporary file in the same directory, and only replaces the live jar after the checksum passes. Writing straight over server.jar means a dropped connection leaves you with a truncated jar and a server that will not start. Keeping the temporary file on the same filesystem makes the final mv atomic.
  • sha256sum -c expects two spaces between the hash and the file name. With one space it reports a formatting error rather than a mismatch, which is a common copy-paste failure.
  • The state file holds the installed build id, so running the script twice in a row downloads nothing the second time.

Run it while the server is stopped, or at least restart straight after: the JVM has the old jar open, and Paper reads its patched classes from the cache directory it builds on first start. The new build takes effect on the next start, when Paperclip notices the new jar and repatches, which adds a few seconds to that first boot.

On macOS sha256sum may not exist; shasum -a 256 -c takes the same input. On Windows the equivalent check is Get-FileHash -Algorithm SHA256 in PowerShell, compared with the sha256 field.

Folia and Velocity specifics#

The API is identical for the other two projects, but what you do with the result is not.

Velocity versions are the proxy's own version numbers, not Minecraft versions. The project lists families 4.0.0 and 3.0.0 with entries such as 4.2.0, 3.5.0 and 3.6.0-SNAPSHOT. Snapshot versions carry many builds; release versions carry very few - 4.2.0 had a single build, number 30, when this was written, and some older release ids such as 3.5.0 list no builds at all, so check before pinning one. The proxy's version is independent of the backend version, and one Velocity build generally supports a range of client versions, so updating it is less fraught than updating Paper - but a proxy update still interrupts every player on the network at once. Schedule it, do not let it happen on a timer in the evening. Note also that the 4.x line asks for Java 25. Building a Velocity proxy network covers how the proxy and backends fit together.

Folia has far fewer versions and builds than Paper, and only versions that PaperMC has ported it to. Its plugin compatibility is the real constraint: a Folia update matters less than whether your plugins declare Folia support at all. Pin Folia builds and test them, as Folia's regionised multithreading explains in more detail.

Forks such as Purpur and Pufferfish are not part of PaperMC's API. They have their own download services with their own URL schemes; Purpur and Pufferfish forks covers what they change and why you might want them.

Running it on a schedule without breaking anything#

The script on its own is safe. What makes automation dangerous is when it runs and what happens next. A sound arrangement on a self-managed Linux machine looks like this:

  1. A backup of the world runs first, every time. A build that corrupts a chunk is rare, but rare is not never, and the backup is cheap. Minecraft world backup and restore covers what to include.
  2. The update script runs with a fixed Minecraft version and the stable channel.
  3. The server restarts at a quiet hour, after a warning in chat.
  4. Something checks that the server came back - the log line Done (...)! For help, type "help" within a few minutes of start - and alerts a person if it did not.
crontab
# 04:30 daily: back up, update within 1.21.11, restart30 4 * * * /home/mc/backup.sh && /home/mc/update-paper.sh 1.21.11 && systemctl restart minecraft

The && chain matters: no backup, no update; a failed download or checksum, no restart. Automating game server updates goes through the general policy - which servers can update themselves and which must not - and restart schedules that help covers the restart side.

On RE:NODE, Minecraft servers are installed with Paper and the matching Java version, the EULA accepted and the world generated, so the first download is done for you. Replacing the jar later is a file operation: upload a build you have checked through the file manager or SFTP (SFTP and the file manager has the details), and the Schedules tab can chain a backup, a chat warning and a restart in order with delays between them. Backup slots come with every Minecraft plan, so the step before an update is one button.

Troubleshooting the errors people actually hit#

`Error: Invalid or corrupt jarfile server.jar`. The file is not a jar. Run head -c 200 server.jar: if it starts with {"ok":false, the download URL was an old v2 one or the request was rejected. Switch to the v3 endpoints and add -f to curl so this cannot happen silently again.

`410 Gone` with `"error":"sunset"`. You are on api.papermc.io/v2. There is no fix except moving to fill.papermc.io/v3. Check scripts, Dockerfiles, Ansible roles and any start script that downloads on boot - they tend to be forgotten because they worked for years.

`jq: error: Cannot index array with "downloads"`. You called the builds list but treated the result as one build. The list is an array; take .[0] first. The single-build endpoints (builds/189, builds/latest) return an object.

The script installed an alpha. It used builds/latest, which ignores channels. Use builds?channel=STABLE and the first element.

Empty array from `?channel=STABLE`. The version has no stable builds yet - common for a week or more after a Minecraft release, and permanent for release candidates. Wait, or stay on the previous version. Do not fall back to alpha automatically.

`sha256sum: WARNING: 1 computed checksum did NOT match`. The download was cut short or altered on the way. Do not install it; delete the file and download again. If it fails repeatedly, compare the file size with the size field in the build JSON - a proxy or captive portal in between is the usual cause.

`UnsupportedClassVersionError` after updating. You moved to a Minecraft version whose jar needs newer Java - 26.x needs Java 25. Read java.version.minimum from the version endpoint before installing.

FAQ#

Does the old api.papermc.io v2 URL still work for old builds?

No. Every v2 path, including direct jar downloads of builds published years ago, returns 410 Gone. Old builds are still available through v3: the version endpoint lists every build id, and each build's JSON has its download URL.

PaperMC's documentation says every request must include one that names your software and gives a contact, and that generic defaults like curl/... are not acceptable. Some requests may still be answered without it, but that is not something to build a script on.

Should the script pick the newest Minecraft version automatically?

No. A new Minecraft version converts the world irreversibly, breaks plugins and requires every player to update their client. Automate builds within a version you chose; change the version yourself, with a backup.

Request https://fill.papermc.io/v3/projects/paper/versions/<version>/builds/<build> and read .downloads."server:default".url. Do not try to construct the URL from the version and build number - in v3 the path contains the file's checksum, so it cannot be guessed.

Does Velocity follow Minecraft version numbers?

No. Velocity versions (3.5.0, 4.2.0 and so on) are the proxy's own releases, and each one supports a range of client versions. Read the release notes for the range rather than matching numbers.

Is the checksum check worth the extra line?

Yes. It is the difference between a server that refuses to update and one that refuses to start. A truncated download is far more common than tampering, and both are caught by the same line.