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.
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,
})
endSet 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.
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.
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.
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.