Image
32 MB max- InputJPEG, PNG, WebP, GIF, AVIF, BMP, TIFF. SVG always refused.
- OutputWebP quality 82 by default, or AVIF, JPEG, PNG, or the original.
- TimingEncoded during the request: the URL returned is final.
If your server can send an HTTP request, it gets a permanent URL back at once: images encoded during the request, video and audio compressed in place, the routes your existing scripts call answered as they are.
{ "status": "ok", "data": { "id": "01K2R7M8QF3V9XZ4B6NCDT0YAH", "url": "https://aperture-app.preview.moncompanion.cloud/aperture-game/01K2R7M8QF3V9XZ4B6NCDT0YAH.webp" }, "meta": { "duration_ms": 142 } }
| Key | Scopes | Uploads · 7 days | Failure rate |
|---|---|---|---|
| lb-phone productionapt_7Hq2kR4n · 2 s ago | upload-imageupload-videoupload-audio | 627 | 0.4% Normal |
| screenshot-basicapt_c4Rt9WbE · 12 min ago | upload-image | 384 | 1.1% Normal |
| MDT policeapt_Q8mZ2xLp · 1 min ago | upload-image | 142 | 6.8% Keep an eye on it Mostly insufficient_scope |
| CI assetsapt_Vn3K8sJd · 3 h ago | manage-assets | 19 | 0% Normal |
1920 × 1080, inside the max box: dimensions unchanged. A 480 px WebP thumbnail is made on the way.
31of 50 GB
Least squares line over the last 28 nights: +180 MB a night. At 100%, uploads stop; delivery never does.
€0
billed by usage: not the traffic to players, not the uploads, not the transcoding.
4routes
v3, v2 and legacy API compatible routes, plus LBUpload, with the same responses.
80-200ms
for a 1080p capture to WebP. The URL returned is already the final one.
100%
of the files online stay served when the quota is full. Only new uploads are refused.
Figures taken from Aperture's code and configuration, not from a campaign. Encoding time depends on the machine hosting the instance.
PerformHttpRequest
lb-phone, screenshot-basic and the routes your scripts already call, answered as they are.
QuickstartHTTP()
GLua, from a server-side addon.
See the sampleHttpClient
Java, from a Paper or Spigot plugin, or a Fabric mod.
See the sampleHttpClient
Java, from a server plugin.
See the samplewebrequest.Enqueue
C#, from an Oxide or Carbon plugin.
See the samplePOST /api/v1/uploads
A Discord bot, a website or your own engine: multipart or base64, on the same two endpoints.
See the sampleGame names belong to their owners and only say which platforms Aperture's API works with. Aperture is not affiliated with or endorsed by them.
A name, a platform. The short identifier goes into every URL.
Fixed for life. A media link resolves through the file's identifier: renaming the organisation breaks none of them.
One key per resource. Limited to what it needs.
A key never carries more than its author is allowed to do.
The API keys screen writes it for you. With your URLs, and your key as soon as it is created.
-- lb-phone/shared/upload.lua, in UploadMethods.Custom Image = { url = "https://aperture-app.preview.moncompanion.cloud/api/v1/uploads", field = "file", headers = { ["Authorization"] = "API_KEY" }, error = { path = "status", value = "error" }, success = { path = "data.url" }, }, -- Video and Audio: same block; leave API_KEY, lb-phone replaces it -- lb-phone/server/apiKeys.lua, one key per type, out of shared/ -- the uploading player still receives it: scope it to uploads only API_KEYS = { Image = "apt_7Hq2kR4n_k3Vb9QzT..." } -- lb-phone/config/config.lua Config.UploadMethod.Image = "Custom" -- add the delivery host to the existing list Config.UploadWhitelistedDomains = { "aperture-app.preview.moncompanion.cloud" }
-- server.lua : PerformHttpRequest has no multipart, so base64 it is PerformHttpRequest("https://aperture-app.preview.moncompanion.cloud/api/v1/uploads/base64", function(status, body) local response = json.decode(body) if status == 200 then print(response.data.url) else print("upload refused: " .. response.error.code) end end, "POST", json.encode({ base64 = dataUri, filename = "capture.png" }), { ["Content-Type"] = "application/json", ["Authorization"] = GetConvar("aperture_key", ""), ["Idempotency-Key"] = captureId, -- a retry never creates a duplicate })
# What the key may do, and the quota left curl https://aperture-app.preview.moncompanion.cloud/api/v1/token \ -H "Authorization: Bearer $APERTURE_KEY" # Upload a file, with no duplicate if the response gets lost curl https://aperture-app.preview.moncompanion.cloud/api/v1/uploads \ -H "Authorization: Bearer $APERTURE_KEY" \ -H "Idempotency-Key: capture-4471" \ -F "file=@screenshot_1730041122.webp"
Without aperture-app.preview.moncompanion.cloud in UploadWhitelistedDomains, the upload succeeds but the photo never shows up in the phone.
Your server uploads a file. The dashboard sees it arrive.
An image is final as soon as the response comes back. A video plays at once, then the compressed file replaces it at the same address.
The type is read from the bytes, never from the extension. Each one has its formats, its limit and its encoding.
The player name comes from the player-metadata header, when the script uploading the file sends it; lb-phone 2.9.0 does not. Nothing checks it: the dashboard shows it as "Supplied by the client".
Every organisation has a ceiling in GB, known in advance. You are warned at 80, 90 and 100%. At the ceiling, new uploads get quota_exceeded and everything online stays served.
A ceiling in GB, known in advance.
An alert at 80, 90 and 100%.
At the ceiling, only uploads stop.
Traffic is measured, never billed.
€0 billed by usage.
Format, quality and maximum size per type, with a before and after test bench on your own images.
Your media served from your own domain: TXT verification, certificate issued on the first request. Links already shared keep working.
13 permissions per member, from View media to Manage storage. An organisation always keeps an owner.
A key never carries more than its author can do. Allowed addresses and a rate limit per key.
Uploads, deliveries, outgoing traffic and refusals. What is not measured reads "Not measured", never zero.
Nothing is deleted by default. Fixed age or unused files, your choice; a file can be kept forever.
Generated from openapi.yaml, with one page per error code and an llms.txt. Documentation in English.
The dashboard speaks both languages. The API answers in English, with stable codes that are never translated.
| Criterion | Aperture | Host billed by usage |
|---|---|---|
| Billing | ||
| Amount known in advance, whatever the traffic | Yes, Fixed monthly price | No |
| Traffic to players with no meter to pay | Yes, Measured, never billed | No |
| Quota full: files online still served | Yes, Only uploads stop | Not verified |
| Integration | ||
| v3, v2, legacy API and LBUpload compatible routes | Yes, Same responses | Not verified |
| Files stored on your FTP or FTPS server | Yes, On request | Not verified |
| Server assets at a stable URL, outside the quota | Yes, Unlimited versions | Not verified |
| Custom domain | PartialRolling out | Not verified |
| Diagnostics | ||
| Stable error codes, one doc page per code | Yes, 34 documented codes | Not verified |
| Retry without duplicates (Idempotency-Key) | Yes, Key kept 24 h | Not verified |
Right-hand column: the usage billing model, described from public offers. Not verified: a point we have not checked against a public offer, so we claim nothing either way. No brand is named: the column describes a model, not a competitor.
"No feature will ever bring usage billing back. It is the reason Aperture exists."
Aperture answers on the paths your scripts already call, with the same response bodies; lb-phone 2.9.0 moves to its Custom method instead. To roll back, put the old configuration back: nothing rewrites your database.