REST API
API Versions
Section titled “API Versions”The Canvas REST API supports versioning through the /api/v{version} URL prefix:
- V2 (Current): The latest stable version, featuring enhanced performance and expanded functionality. All integrations have to use this version.
- V1 (Removed): The legacy version, which has since been removed as of November 5th 2025.
V2 API Endpoints
Section titled “V2 API Endpoints”Base URL: https://canvasmc.io/api/v2
GET /projects
Returns a list of all available projects and information about them.
Response
Section titled “Response”{ "projects": [ { "slug": "canvas", "ciJob": "Canvas", "ciJobUrl": "https://jenkins.canvasmc.io/job/Canvas/", "javadocBaseUrl": "https://maven.canvasmc.io/javadoc/snapshots/io/canvasmc/canvas/canvas-api" }, { "slug": "horizon", "ciJob": "Horizon", "ciJobUrl": "https://jenkins.canvasmc.io/job/Horizon/", "javadocBaseUrl": "https://maven.canvasmc.io/javadoc/releases/io/canvasmc/horizon/core" } ]}GET /builds
Returns a list of all builds. This is identical to the /builds/all endpoint.
Query Parameters
Section titled “Query Parameters”Required
Section titled “Required”project(string) - The project slug for which to display builds
Optional
Section titled “Optional”channel(string) - Filter builds by channel versionexperimental(boolean) - Include experimental builds
Response
Section titled “Response”[ { "buildNumber": 1337, "url": "https://jenkins.canvasmc.io/job/Canvas/1337/", "downloadUrl": "https://jenkins.canvasmc.io/job/Canvas/1337/artifact/canvas-server/build/libs/canvas-build.1337.jar", "channelVersion": "1.21.4", "timestamp": 1739339329161, "isExperimental": true, "commit": { // [!code --] "message": "Hello world!", // [!code --] "hash": "3768ac53eb2671853145bd077ade0579e13741ed" // [!code --] }, // [!code --] "commits": [ { "message": "Hello world!", "hash": "3768ac53eb2671853145bd077ade0579e13741ed" }, { "message": "Why are you reading this?", "hash": "895b307dcc7c6fbb040dc7bd26d9a754e03cf8c7" } ] }, // ...]GET /builds/all
Returns a list of all builds.
Query Parameters
Section titled “Query Parameters”Required
Section titled “Required”project(string) - The project slug for which to display builds
Optional
Section titled “Optional”channel(string) - Filter builds by channel versionexperimental(boolean) - Include experimental builds
Response
Section titled “Response”[ { "buildNumber": 1337, "url": "https://jenkins.canvasmc.io/job/Canvas/1337/", "downloadUrl": "https://jenkins.canvasmc.io/job/Canvas/1337/artifact/canvas-server/build/libs/canvas-build.1337.jar", "channelVersion": "1.21.4", "timestamp": 1739339329161, "isExperimental": true, "commit": { // [!code --] "message": "Hello world!", // [!code --] "hash": "3768ac53eb2671853145bd077ade0579e13741ed" // [!code --] }, // [!code --] "commits": [ { "message": "Hello world!", "hash": "3768ac53eb2671853145bd077ade0579e13741ed" }, { "message": "Why are you reading this?", "hash": "895b307dcc7c6fbb040dc7bd26d9a754e03cf8c7" } ] }, // ...]GET /builds/latest
Returns the latest build.
Query Parameters
Section titled “Query Parameters”Required
Section titled “Required”project(string) - The project slug for which to display the build
Optional
Section titled “Optional”channel(string) - Filter builds by channel versionexperimental(boolean) - Include experimental builds
Response
Section titled “Response”{ "buildNumber": 1337, "url": "https://jenkins.canvasmc.io/job/Canvas/1337/", "downloadUrl": "https://jenkins.canvasmc.io/job/Canvas/1337/artifact/canvas-server/build/libs/canvas-build.1337.jar", "channelVersion": "1.21.4", "timestamp": 1739339329161, "isExperimental": true, "commit": { // [!code --] "message": "Hello world!", // [!code --] "hash": "3768ac53eb2671853145bd077ade0579e13741ed" // [!code --] }, // [!code --] "commits": [ { "message": "Hello world!", "hash": "3768ac53eb2671853145bd077ade0579e13741ed" }, { "message": "Why are you reading this?", "hash": "895b307dcc7c6fbb040dc7bd26d9a754e03cf8c7" } ]}GET /jd
Redirects to the Javadocs page for the latest build of the specified project (or filtered by channel/experimental).
Query Parameters
Section titled “Query Parameters”Required
Section titled “Required”project(string) - The project slug for which to display JDs
Optional
Section titled “Optional”channel(string) – The channel version to use. Defaults to latest.experimental(boolean) – Include experimental builds (trueorfalse). Defaults tofalse.
Response
Section titled “Response”Example redirect URL if project=canvas, channel=1.21.8 and experimental=false:
https://maven.canvasmc.io/javadoc/snapshots/io/canvasmc/canvas/canvas-api/1.21.8-R0.1-SNAPSHOT- If version is not provided, the endpoint uses the channel version from the latest build (respecting the experimental flag to allow experimental versions).
- This endpoint does not return JSON; it performs an HTTP redirect to the Javadocs page.
- Use this endpoint to quickly navigate to the Javadocs without manually looking up the build number.
Java API Client
Section titled “Java API Client”Bellow is a Java example file for interacting with the Canvas V2 API.
Click to view Java API client
/** * Client for interacting with the Canvas V2 REST API. * * <p>URL: <a href="https://canvasmc.io/api/v2">https://canvasmc.io/api/v2</a></p> * * <p>This client provides methods to fetch builds, retrieve the latest stable or experimental versions, * and inspect commits associated with each build.</p> * * <p>All returned collections are non-null but may be empty unless otherwise noted.</p> */public final class ApiClient { /** * The base URL for API access */ private static final String BASE_URL = "https://canvasmc.io/api/v2"; /** * The HTTP client */ private static final HttpClient CLIENT = HttpClient.newHttpClient();
private final String project;
/** * Constructs a new API client for a provided project * * @param project * the project id */ public ApiClient(final @NonNull String project) { this.project = project.toLowerCase(); }
/** * Fetches all builds for a specific Minecraft version. * * <p>If {@code experimental} is true, experimental builds are included. * Otherwise, only stable builds are returned.</p> * * @param minecraftVersion * the target Minecraft version (non-null, but may be blank to fetch all builds) * @param experimental * whether to include experimental builds in the result * * @return a non-null list of matching builds (may be empty) * * @throws IOException * if the API request fails * @throws InterruptedException * if the HTTP request is interrupted */ public @NonNull List<Build> getAllBuilds(String minecraftVersion, boolean experimental) throws IOException, InterruptedException { StringBuilder url = new StringBuilder(BASE_URL + "/builds?project=" + project);
if (minecraftVersion != null && !minecraftVersion.isBlank()) { url.append("&channel=").append(minecraftVersion); } if (experimental) { url.append("&experimental=true"); }
String json = sendRequest(url.toString()); List<Build> builds = parseBuildsArray(json); builds.sort(Comparator.comparingInt(Build::buildNumber)); return builds; }
/** * Returns the latest build for the specified Minecraft version. * * <p>If {@code includeExperimental} is true, experimental builds are considered. * Otherwise, only stable builds are used when determining the latest version.</p> * * @param minecraftVersion * the target Minecraft version * @param includeExperimental * whether to include experimental builds in the search * * @return the latest build matching the filters, or {@code null} if none exist * * @throws IOException * if the API request fails * @throws InterruptedException * if the HTTP request is interrupted */ public @Nullable Build getLatestBuildForVersion(String minecraftVersion, boolean includeExperimental) throws IOException, InterruptedException {
List<Build> builds = getAllBuilds(minecraftVersion, includeExperimental); if (builds.isEmpty()) { return null; }
return builds.stream() .max(Comparator.comparingInt(Build::buildNumber)) .orElse(null); }
/** * Returns the latest <b>stable</b> build for the specified Minecraft version. * * <p>This is equivalent to calling * {@link #getLatestBuildForVersion(String, boolean)} with {@code includeExperimental = false}.</p> * * @param minecraftVersion * the target Minecraft version * * @return the latest stable build, or {@code null} if none exist * * @throws IOException * if the API request fails * @throws InterruptedException * if the HTTP request is interrupted */ public @Nullable Build getLatestBuildForVersion(String minecraftVersion) throws IOException, InterruptedException { return getLatestBuildForVersion(minecraftVersion, false); }
/** * Returns the latest build across <b>all Minecraft versions</b>. * * @param experimental * whether to allow experimental builds to be returned * * @return the latest available build * * @throws IOException * if the API request fails * @throws InterruptedException * if the HTTP request is interrupted */ public @NonNull Build getLatestBuild(boolean experimental) throws IOException, InterruptedException { String url = BASE_URL + "/builds/latest?project=" + project + (experimental ? "&experimental=true" : ""); String json = sendRequest(url); return parseSingleBuild(json); }
/** * Returns the build across <b>all Minecraft versions</b> related to the provided build number. * * @param buildNum * the build number * * @return the build associated with the provided build number * * @throws IOException * if the API request fails * @throws InterruptedException * if the HTTP request is interrupted */ public @Nullable Build getBuild(int buildNum) throws IOException, InterruptedException { String json = sendRequest(BASE_URL + "/builds?project=" + project + "&experimental=true"); List<Build> builds = parseBuildsArray(json); builds.sort(Comparator.comparingInt(Build::buildNumber)); return builds.stream() .filter((build) -> build.buildNumber == buildNum) .findFirst().orElse(null); }
private String sendRequest(String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Accept", "application/json") .GET() .build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { throw new IOException("Failed to fetch from Canvas API: " + response.statusCode()); } return response.body(); }
private @NonNull List<Build> parseBuildsArray(@NonNull String json) { List<Build> builds = new ArrayList<>(); int start = json.indexOf("["); int end = json.lastIndexOf("]"); if (start < 0 || end < 0) return builds;
String arrayContent = json.substring(start + 1, end); String[] objects = splitObjects(arrayContent);
for (String obj : objects) { obj = obj.strip(); if (!obj.isEmpty()) { builds.add(parseSingleBuild(obj)); } } return builds; }
private @NonNull Build parseSingleBuild(String json) { int buildNumber = extractIntElse(json, "buildNumber", -1); String url = extractString(json, "url"); String downloadUrl = extractString(json, "downloadUrl"); String channelVersion = extractString(json, "channelVersion"); long timestamp = extractLong(json, "timestamp"); boolean experimental = extractBoolean(json, "isExperimental"); ChannelType channelType = buildNumber == -1 ? ChannelType.LOCAL : experimental ? ChannelType.BETA : ChannelType.STABLE;
List<Commit> commits = parseCommits(json); return new Build(buildNumber, url, downloadUrl, channelVersion, timestamp, channelType, commits.toArray(new Commit[0])); }
private @NonNull List<Commit> parseCommits(@NonNull String json) { List<Commit> commits = new LinkedList<>(); String key = "\"commits\":"; int start = json.indexOf(key); if (start < 0) return commits;
start = json.indexOf("[", start); int end = json.indexOf("]", start); if (start < 0 || end < 0) return commits;
String arrayContent = json.substring(start + 1, end); String[] objects = splitObjects(arrayContent);
for (String obj : objects) { String message = extractString(obj, "message"); String hash = extractString(obj, "hash"); if (message != null && hash != null) { commits.add(new Commit(message, hash)); } } return commits; }
private @NonNull String @NonNull [] splitObjects(@NonNull String json) { List<String> objects = new LinkedList<>(); int braceCount = 0; int lastSplit = 0;
for (int i = 0; i < json.length(); i++) { char c = json.charAt(i); if (c == '{') braceCount++; else if (c == '}') braceCount--;
if (braceCount == 0 && c == '}') { objects.add(json.substring(lastSplit, i + 1)); lastSplit = i + 2; // skip comma + space } } return objects.toArray(new String[0]); }
private @Nullable String extractString(@NonNull String json, String key) { String k = "\"" + key + "\":"; int idx = json.indexOf(k); if (idx < 0) return null;
idx = json.indexOf('"', idx + k.length()); if (idx < 0) return null;
int end = json.indexOf('"', idx + 1); if (end < 0) return null;
return json.substring(idx + 1, end); }
private int extractInt(String json, String key) { String value = extractNumber(json, key); return value == null ? 0 : Integer.parseInt(value); }
private int extractIntElse(String json, String key, int fallback) { String value = extractNumber(json, key); return value == null ? fallback : Integer.parseInt(value); }
private long extractLong(String json, String key) { String value = extractNumber(json, key); return value == null ? 0L : Long.parseLong(value); }
private boolean extractBoolean(@NonNull String json, String key) { String k = "\"" + key + "\":"; int idx = json.indexOf(k); if (idx < 0) return false;
int start = idx + k.length(); int end = json.indexOf(',', start); if (end < 0) end = json.indexOf('}', start); if (end < 0) return false;
return Boolean.parseBoolean(json.substring(start, end).trim()); }
private @Nullable String extractNumber(@NonNull String json, String key) { String k = "\"" + key + "\":"; int idx = json.indexOf(k); if (idx < 0) return null;
int start = idx + k.length(); int end = json.indexOf(',', start); if (end < 0) end = json.indexOf('}', start); if (end < 0) return null;
return json.substring(start, end).trim(); }
/** * The release channel of this build */ // note: modified from original form to remove text color refs public enum ChannelType { STABLE(), BETA(), LOCAL(), UNKNOWN();
ChannelType() { } }
/** * Represents a Jenkins build * * @param buildNumber * the build number * @param url * the URL for this associated build * @param downloadUrl * the download URL * @param channelVersion * the channel version for this build * @param timestamp * the timestamp of the associated build * @param channelType * the channel of this build * @param commits * an array of commits in this build, can be empty */ public record Build( int buildNumber, String url, String downloadUrl, String channelVersion, long timestamp, ChannelType channelType, Commit[] commits ) { public boolean hasChanges() { return this.commits.length > 0; } }
/** * Represents a GitHub Commit * * @param message * the commit message * @param hash * the commit hash */ public record Commit(String message, String hash) { }}