DocumentationAPI v1
Guide · Integrations

lb-phone

The configuration block, and why it targets Custom rather than a hosted method.

Use the Custom method

Config.UploadMethod accepts the hosted methods lb-phone ships with, and Custom. Point at Custom.

In lb-phone 2.9.0, the release this guide was written against, every hosted method holds url = "PRESIGNED_URL": lb-phone asks that host for an upload address, and the block sends no Authorization header. None of them can be pointed here, and Custom is the way in.

Custom is fully parameterised: URL, form field, headers, the path to the URL in the response and the path that signals failure. It has not changed shape across lb-phone versions, so a block written today keeps working, and it puts no compatibility layer between the phone and the native API.

The block

In lb-phone/shared/upload.lua, inside UploadMethods.Custom:

Lua
Custom = {
    Video = {
        url = "https://your-instance/api/v1/uploads",
        field = "file",
        headers = { ["Authorization"] = "API_KEY" },
        error   = { path = "status", value = "error" },
        success = { path = "data.url" },
    },
    Image = {
        url = "https://your-instance/api/v1/uploads",
        field = "file",
        headers = { ["Authorization"] = "API_KEY" },
        error   = { path = "status", value = "error" },
        success = { path = "data.url" },
    },
    Audio = {
        url = "https://your-instance/api/v1/uploads",
        field = "file",
        headers = { ["Authorization"] = "API_KEY" },
        error   = { path = "status", value = "error" },
        success = { path = "data.url" },
    },
}

Leave API_KEY exactly as written: it is a placeholder, and lb-phone swaps it for the key of that kind from lb-phone/server/apiKeys.lua. The tokens go there, in a server-side file, rather than in shared/, which every player downloads when they join (read Three keys, treated as public for what that does not protect):

Lua
API_KEYS = {
    Video = "YOUR_VIDEO_TOKEN",
    Image = "YOUR_IMAGE_TOKEN",
    Audio = "YOUR_AUDIO_TOKEN",
}

Then in lb-phone/config/config.lua:

Lua
Config.UploadMethod.Video = "Custom"
Config.UploadMethod.Image = "Custom"
Config.UploadMethod.Audio = "Custom"

Allow the delivery host, or nothing is saved

This is the step that gets missed, and it fails in the most confusing way available: uploads succeed, the API returns a URL, the file is served correctly, and the photo still never appears in the phone.

lb-phone keeps its own allowlist of hosts it will accept an image from, so that somebody with devtools open cannot inject an arbitrary link. Yours is not on it.

In lb-phone/config/config.lua:

Lua
Config.UploadWhitelistedDomains = {
    -- the hosts already listed stay as they are
    "cdn.your-instance", -- your delivery host, exactly as it appears in the URL
}

Use the delivery host, the one media URLs are served from, not the dashboard host the upload was posted to. They are deliberately different origins.

If you skip it the server console says so, once per attempt:

Text
[LB Phone] [ERROR]: <id> tried to save an image with a link that is not allowed:
https://cdn.your-instance/your-org/01K2R7....webp

Config.ExternalWhitelistedDomains, just above it, governs images pasted from elsewhere. It is empty by default, which means no restriction; if you ever fill it, the same host has to go in there too.

Why those two paths

success.path = "data.url" is where the URL sits in our response. It is also where a v3 compatible response puts it, which is why a configuration written for the v3 routes works against the native endpoint with no facade in between.

error.path = "status" with value = "error" is how the phone detects failure. Our envelope carries a single top-level status that is either ok or error, specifically so this check has something stable to read.

Three keys, treated as public

apiKeys.lua keeps the keys out of shared/, so they are not in the files every player downloads. It does not keep them from players: lb-phone sends each upload from the player's game, so the key of that kind reaches the game of the player who uploads, and a curious player can read it there.

So treat every key in that file as exposed. Use three, each scoped to uploading its own kind and nothing else, and give them to no other script. When one turns up where it should not, and that does happen, an image key that cannot upload video is a much smaller problem than one key that can do everything: revoke it and put a new one in its place.

Add no other header to the Custom block. Its headers are the same fixed strings on every upload, so a fixed Idempotency-Key would return the first photo's URL for every later upload, for a day. A player-metadata header filled from PLAYER_DATA fails too: the game's browser refuses to send a header holding a character outside Latin-1, which blocks every upload of a player whose name has one, and sends accented letters in a form Aperture cannot decode.

Video settings on the phone side

Config.Video.Bitrate and Config.Video.MaxSize still govern what the phone records and sends. Aperture re-encodes afterwards, so you can leave the phone generous and let the compression profile decide the final size. The clip is playable at its URL the moment the upload returns, before that re-encode finishes.