CanvasMC LogoCanvasMC Docs

REST API

Comprehensive documentation for Canvas's REST API, including endpoints, parameters, and response formats.

Authors:Dueris's avatarDueris
Reading time:2 min read

API Versions

The Canvas REST API supports versioning through the /api/v{version} URL prefix. Currently, two API versions are available:

  • V2 (Current): The latest stable version, featuring enhanced performance and expanded functionality. All new integrations should use this version.
  • V1 (Deprecated, to be removed soon): Legacy version maintained for existing integrations. This version will be discontinued soon.

For backwards compatibility with already existing integrations, route /api automatically redirects to the /api/v1 endpoint.

V2 API Endpoints

Base URL: https://canvasmc.io/api/v2

GET /builds

Returns a list of all recent builds.

Query Parameters

  • minecraft_version (string) - Filter builds by Minecraft version
  • experimental (boolean) - Include experimental builds

Response

Example 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",
    "minecraftVersion": "1.21.4",
    "timestamp": 1739339329161,
    "isExperimental": true,
    "commit": { 
      "message": "Hello world!", 
      "hash": "3768ac53eb2671853145bd077ade0579e13741ed"
    }, 
    "commits": [
      {
        "message": "Hello world!",
        "hash": "3768ac53eb2671853145bd077ade0579e13741ed"
      },
      {
        "message": "Why are you reading this?",
        "hash": "895b307dcc7c6fbb040dc7bd26d9a754e03cf8c7"
      }
    ]
  },
  // ...
]

The commit property has been deprecated in favor of the commits array.

GET /builds/latest

Returns the latest build.

Query Parameters

  • experimental (boolean) - Include experimental builds

Response

Example 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",
  "minecraftVersion": "1.21.4",
  "timestamp": 1739339329161,
  "isExperimental": true,
  "commit": { 
    "message": "Hello world!", 
    "hash": "3768ac53eb2671853145bd077ade0579e13741ed"
  }, 
  "commits": [
    {
      "message": "Hello world!",
      "hash": "3768ac53eb2671853145bd077ade0579e13741ed"
    },
    {
      "message": "Why are you reading this?",
      "hash": "895b307dcc7c6fbb040dc7bd26d9a754e03cf8c7"
    }
  ]
}

The commit property has been deprecated in favor of the commits array.

GET /jd

Redirects to the Javadocs page for the latest Canvas build (or filtered by version/experimental).

Query Parameters

  • version (string, optional) – The Minecraft version to use. Defaults to the version of the latest build.
  • experimental (boolean, optional) – Include experimental builds (true or false). Defaults to false.

The query parameters are optional, and with no params specified, it defaults to the latest stable API version.

Response

Example redirect URL if version=1.21.8 and experimental=false:

https://maven.canvasmc.io/javadoc/snapshots/io/canvasmc/canvas/canvas-api/1.21.8-R0.1-SNAPSHOT

Notes

  • If version is not provided, the endpoint uses the Minecraft 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.

V1 API Endpoints

The V1 API has been deprecated. While still accessible, we strongly recommend migrating to V2 API for all new and existing integrations. For historical reference, the V1 API documentation can be found here.

Java API Client

Bellow is a Java example file for interacting with the Canvas V2 API.

The API client is authored by Dueris and used in the Sculptor project for Canvas

/**
 * 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 final HttpClient CLIENT = HttpClient.newHttpClient();

    /**
     * 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");

        boolean hasQuery = false;
        if (minecraftVersion != null && !minecraftVersion.isBlank()) {
            url.append("?minecraft_version=").append(minecraftVersion);
            hasQuery = true;
        }
        if (experimental) {
            url.append(hasQuery ? "&" : "?").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" + (experimental ? "?experimental=true" : "");
        String json = sendRequest(url);
        return parseSingleBuild(json);
    }

    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 = extractInt(json, "buildNumber");
        String url = extractString(json, "url");
        String downloadUrl = extractString(json, "downloadUrl");
        String mcVersion = extractString(json, "minecraftVersion");
        long timestamp = extractLong(json, "timestamp");
        boolean experimental = extractBoolean(json, "isExperimental");

        List<Commit> commits = parseCommits(json);
        return new Build(buildNumber, url, downloadUrl, mcVersion, timestamp, experimental, 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 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();
    }

    /**
     * Represents a Jenkins build
     *
     * @param buildNumber      the build number
     * @param url              the URL for this associated build
     * @param downloadUrl      the download URL
     * @param minecraftVersion the Minecraft version for this build
     * @param timestamp        the timestamp of the associated build
     * @param isExperimental   if the build is marked as experimental
     * @param commits          an array of commits in this build, can be empty
     */
    public record Build(
            int buildNumber,
            String url,
            String downloadUrl,
            String minecraftVersion,
            long timestamp,
            boolean isExperimental,
            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) {
    }
}
Edit on GitHub

Last updated on