DocumentationAPI v1
Guide · Getting started

Uploading

Multipart, base64, metadata, retries and what happens to your file.

Multipart

The normal path. Field name is file.

Shell
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.

Lua
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:

JSON
{ "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.

Text
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.