Uploading
Multipart, base64, metadata, retries and what happens to your file.
Multipart
The normal path. Field name is file.
curl -X POST https://your-instance/api/v1/uploads \
-H "Authorization: ${APERTURE_KEY}" \
-F "file=@clip.mp4" \
-F "metadata={\"scene\":\"heist\"}"Base64
PerformHttpRequest in Lua has no multipart support at all, so this is the only
way a resource can upload without a helper script. A bare base64 string and a
data URI are both accepted.
PerformHttpRequest("https://your-instance/api/v1/uploads/base64", function(status, body)
local response = json.decode(body)
if status == 200 then
print(response.data.url)
else
print("upload failed: " .. (response.error and response.error.code or status))
end
end, "POST", json.encode({
base64 = "data:image/png;base64,...",
filename = "screenshot.png",
}), {
["Content-Type"] = "application/json",
["Authorization"] = "YOUR_TOKEN",
})What happens to the file
The kind is decided by reading the bytes, never by the file extension and never
by the endpoint you called. A file claiming to be a PNG that is not one is
refused with unsupported_media_type.
Everything is re-encoded. That is a compression feature and a security boundary at the same time: a file that is both a valid image and a valid script does not survive being decoded and written back out.
Images encode inside the request, roughly 80 to 200 ms for a screenshot. The URL you receive is final.
Video and audio return immediately with status: "processing". The URL
already works and already serves your file; a queued job then re-encodes it and
replaces the bytes at the same address. The URL never changes, which is why a
clip can be shared before it has finished shrinking.
SVG is refused. It is a script container wearing an image extension.
Metadata
metadata is a JSON object passed through untouched and handed back on every
read. Use it for whatever your resource needs to correlate later.
It is bounded, generously for anything a resource records: at most 16 KB as
compact JSON, 8 levels deep, and 100 entries counted at every level. Beyond
that the upload is refused with invalid_metadata before anything is stored,
and error.details says which bound was crossed:
{ "limit": "entries", "maximum": 100, "actual": 140 }The same bounds apply to PATCH /api/v1/media/{id}.
The file name is kept as original_name, but only ever as a label: its last
path segment, without control characters, cut to 255 characters.
If your resource sends a player-metadata header, the uploader is recorded and
shown in the library. The block in the lb-phone guide
sends only the key: the phone's interface cannot put every player's name in a
header, and that guide says why.
Retries
PerformHttpRequest has no dependable timeout, so a lost response is a real
possibility and a naive retry bills you twice for one screenshot.
Send an Idempotency-Key header and the retry returns the media the first call
created instead of storing a second copy. Keys live for 24 hours. Use one stable request ID, such as heist-2026-08-09-4471, for every retry of the same upload.
Idempotency-Key: ${REQUEST_ID}Limits
Sizes are capped per kind and reported by /api/v1/status, so a client can read
them rather than discover them. Exceeding one is a 413 with file_too_large,
and a full quota is a 413 with quota_exceeded. The two are different codes
on purpose: one is a file to shrink, the other is a conversation with your
administrator.
Rate limiting is per token, not per address. Every player on a server uploads through the same IP, and an address limit would throttle a busy community for being busy while doing nothing about a key leaked elsewhere.