DocumentationAPI v1
Guide · Integrations

Other games and engines

Uploading from Garry's Mod, Minecraft, Hytale, Rust or any HTTP client.

FiveM, RedM and FiveM Enhanced get ready-made integrations: the lb-phone and screenshot-basic guides, and the routes older scripts already call. Every other game talks to the same API over plain HTTP, and this page gives one sample per engine.

Each sample is a starting point, not a finished plugin. The request it sends was checked against Aperture's upload endpoints; the code around it follows each engine's documented HTTP API, and you should run it on a test server of your own before relying on it. File paths, logging and error handling are yours to adapt.

Two endpoints

Endpoint Body Use it when
POST /api/v1/uploads multipart/form-data, the file in the field file The client builds multipart: cURL, Node, Python, a Discord bot
POST /api/v1/uploads/base64 JSON: {"base64": "...", "filename": "..."} The engine's HTTP call only sends a string body: GLua, Oxide, a plain Java HttpClient

Both answer 200 with the same JSON, and data.url is the permanent link. base64 takes a bare base64 string or a data URI. Uploading covers metadata, retries without duplicates, and the size limits.

The key goes in the Authorization header, as Bearer apt_... or raw, the way Authentication describes. A refused upload answers with a stable code in error.code, such as invalid_token, file_too_large or quota_exceeded.

The key stays on the server

Every sample on this page runs on the game server. A key inside a client mod, a resource pack or any file a player downloads is a public key: anyone can upload with it until it is revoked.

Give the server a key limited to uploads, which is what the "Server script" preset at sign-up creates, and read it from a protected convar, an environment variable or a configuration file that never leaves the server.

Garry's Mod

HTTP() sends a string body, so the file goes to the base64 endpoint. The code runs in the server realm only (lua/autorun/server/), and the key lives in a convar marked FCVAR_PROTECTED, which the server never sends to clients.

lua/autorun/server/aperture.lua
local key = CreateConVar("aperture_key", "", bit.bor(FCVAR_PROTECTED, FCVAR_ARCHIVE), "Aperture API key")

-- path is relative to garrysmod/data, for example "screenshots/heist.jpg".
function ApertureUpload(path)
    local bytes = file.Read(path, "DATA")

    if not bytes then
        print("[aperture] nothing to upload at " .. path)
        return
    end

    HTTP({
        url = "https://your-instance/api/v1/uploads/base64",
        method = "POST",
        type = "application/json",
        headers = { Authorization = key:GetString() },
        body = util.TableToJSON({
            -- true: one line, without a line break every 76 characters
            base64 = util.Base64Encode(bytes, true),
            filename = string.GetFileFromFilename(path),
        }),
        success = function(code, body)
            local response = util.JSONToTable(body) or {}

            if code == 200 then
                print("[aperture] " .. response.data.url)
            else
                print("[aperture] upload refused: " .. ((response.error or {}).code or code))
            end
        end,
        failed = function(reason)
            print("[aperture] request failed: " .. reason)
        end,
    })
end

Set the key from the server console or server.cfg with aperture_key "apt_...". HTTP() does nothing before the server's first tick, so call ApertureUpload from a hook or a console command, not while the file loads.

Minecraft

A current Paper, Spigot or Fabric server runs on Java 17 or later, whose java.net.http.HttpClient sends the request without any library. The class below posts to the base64 endpoint, which spares you building multipart by hand, and returns at once: the upload never holds up the server thread.

ApertureUploader.java
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;

public final class ApertureUploader {
    private static final HttpClient CLIENT = HttpClient.newHttpClient();

    private final URI endpoint;
    private final String key;

    public ApertureUploader(String instance, String key) {
        this.endpoint = URI.create(instance + "/api/v1/uploads/base64");
        this.key = key;
    }

    /** Sends the file without blocking the caller: the future completes with Aperture's JSON answer. */
    public CompletableFuture<HttpResponse<String>> upload(Path file) throws IOException {
        String bytes = Base64.getEncoder().encodeToString(Files.readAllBytes(file));
        // Only characters that need no escaping, so the name can go into the JSON as is.
        String name = file.getFileName().toString().replaceAll("[^A-Za-z0-9._-]", "_");
        String json = "{\"base64\":\"" + bytes + "\",\"filename\":\"" + name + "\"}";

        HttpRequest request = HttpRequest.newBuilder(endpoint)
            .header("Authorization", "Bearer " + key)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString());
    }
}

In a Paper or Spigot plugin, read the key from the plugin's config.yml, and come back to the main thread before touching the Bukkit API with the answer: the future completes on one of the HTTP client's threads.

MyPlugin.java
ApertureUploader aperture = new ApertureUploader("https://your-instance", getConfig().getString("aperture.key"));

aperture.upload(getDataFolder().toPath().resolve("renders/spawn.png")).thenAccept(response ->
    Bukkit.getScheduler().runTask(this, () -> getLogger().info(response.statusCode() + " " + response.body()))
);

response.body() is the JSON described above; Gson, which Paper and Spigot ship, reads data.url out of it.

Hytale

Hytale server plugins are written in Java, so ApertureUploader from the Minecraft section works unchanged: java.net.http belongs to the JDK, not to the game. Read the key from your plugin's own configuration, and handle the answer on a thread the game allows, as on Minecraft.

Rust

An Oxide (uMod) or Carbon plugin sends HTTP with webrequest.Enqueue, which takes a string body, so the bytes go to the base64 endpoint. Where they come from is up to the plugin: an image it rendered, the picture on a sign. The key sits in the plugin's configuration file, on the server only.

ApertureUpload.cs
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core.Libraries;

namespace Oxide.Plugins
{
    [Info("Aperture Upload", "your-name", "0.1.0")]
    [Description("Uploads files to Aperture")]
    public class ApertureUpload : RustPlugin
    {
        private const string Endpoint = "https://your-instance/api/v1/uploads/base64";

        // oxide/config/ApertureUpload.json: { "ApiKey": "apt_..." }
        private string ApiKey => Config["ApiKey"] as string;

        private void Upload(byte[] bytes, string filename)
        {
            string body = JsonConvert.SerializeObject(new Dictionary<string, string>
            {
                ["base64"] = System.Convert.ToBase64String(bytes),
                ["filename"] = filename,
            });

            var headers = new Dictionary<string, string>
            {
                ["Authorization"] = ApiKey,
                ["Content-Type"] = "application/json",
            };

            webrequest.Enqueue(Endpoint, body, (code, response) =>
            {
                if (code != 200)
                {
                    PrintWarning($"upload refused: {code} {response}");
                    return;
                }

                Puts(response);
            }, this, RequestMethod.POST, headers);
        }
    }
}

Any HTTP client

A Discord bot, a website's backend, a launcher or an engine of your own: anything that can send a multipart request uses the first endpoint, with the file in the field file.

curl -X POST https://your-instance/api/v1/uploads \
  -H "Authorization: Bearer ${APERTURE_KEY}" \
  -F "file=@screenshot.png"
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append(
    "file",
    new Blob([await readFile("screenshot.png")]),
    "screenshot.png",
);

const response = await fetch("https://your-instance/api/v1/uploads", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.APERTURE_KEY}` },
    body: form,
});
const { data, error } = await response.json();

console.log(response.ok ? data.url : `upload refused: ${error.code}`);

The JavaScript runs on Node 18 or later, or any runtime with fetch and FormData. Leave Content-Type unset: fetch writes it, with the multipart boundary. Never run it in a browser page: the key would ship to every visitor. Upload from your backend, and hand the page the URL.