DocumentationAPI v1
API 1.0 · OpenAPI 3.1.0

API reference. Read from the schema, never copied.

Every route of the native API is described here, along with the routes compatible with existing scripts, such as screenshot-basic calls and older lb-phone releases. All of it comes from openapi.yaml, which a test keeps in agreement with the server's routes.

Base URL https://aperture-app.preview.moncompanion.cloud
AccessAPI keyBearer, raw key or ?apiKey=
FormatJSONstatus, data or error, meta
Endpoints30native API and compatible routes
Codes34stable, never translated

Descriptions come from the schema and stay in English, like the API's responses.

Two surfaces live here and they are not equal citizens.

The native API under /api/v1 is the one we own and document. It carries the response envelope, and it is what this schema describes in full.

The compatibility facades answer on the exact paths already baked into deployed game server scripts, so migrating is a URL change and nothing else. Their response shapes belong to somebody else and are never amended, not even to add a helpful field: lb-phone walks its response with a dot path, so a new key can break a client that a status code check would call healthy.

Authentication

Three carriers are accepted, because deployed scripts use all three:

  • Authorization: Bearer <token>
  • Authorization: <token>, raw, which is what most FiveM resources send
  • ?apiKey=<token>, for screenshot-basic calls that cannot set headers

Errors

Every error carries a stable machine code, never a translated sentence, and a docs link to the page describing it. Branch on the code.

Rate limiting

Every route that takes a token is limited per token, per minute; the ticket upload route, which takes none, is limited per address. A request over the limit answers 429 with the code rate_limited and a Retry-After header: the whole number of seconds to wait, never zero. A request sent once that many seconds have passed is not refused for the same window. The header is sent on every surface, the compatibility routes included, whose bodies have no room for the delay. X-RateLimit-Limit, X-RateLimit-Remaining (0) and X-RateLimit-Reset come with it. A token route also sends those three on every request it lets through. X-RateLimit-Reset is the number of seconds until the window empties, not a timestamp. It is counted in whole seconds, so it can be up to a second short, which is why Retry-After is always one more than it on a refusal. The native body's error.details.retry_after_seconds is the same whole-second count. On the /api routes all four headers are exposed to cross-origin callers, so a NUI page can read them.

Maintenance

During a maintenance window a refused request answers 503 with the code maintenance, on every surface in its own error shape, with a Retry-After header (whole seconds, 1 to 900) and Cache-Control: no-store. The native body adds error.details.retry_after_seconds, error.details.expected_end_at and error.details.media_served. Some maintenance keeps stored files served: then read requests (GET, HEAD) keep answering and only uploads and changes are refused, which media_served: true says. GET /api/v1/status answers throughout.

Timing

meta.duration_ms is a server time: bootstrap, routing and handling. It excludes the final JSON encode and the network. The Server-Timing header breaks it down further and renders natively in browser devtools.

First call

Check the key first: the answer gives its scopes and the quota left. Then send a file; the URL is in data.url.

curl https://aperture-app.preview.moncompanion.cloud/api/v1/token \
  -H "Authorization: Bearer $APERTURE_KEY"
curl -X POST https://aperture-app.preview.moncompanion.cloud/api/v1/uploads \
  -H "Authorization: Bearer $APERTURE_KEY" \
  -F "file=@screenshot.png"

Uploads

The native upload surface.

POST /api/v1/uploads Upload a file

Parameters

NameInDescription
Idempotency-KeyOptional header Returns the media the first call created instead of storing a second copy. PerformHttpRequest in Lua has no dependable timeout, so a retry after a lost response would otherwise be billed twice. Keys live for 24 hours and are scoped to the organisation and token.
player-metadataOptional header {"identifier":"license:...","name":"...","identifiers":["license:...","discord:..."]}. Read when the uploading script sends it; lb-phone 2.9.0 does not. Recorded so the library can say who took a screenshot rather than only when. Malformed JSON is ignored rather than rejected: attribution is a nice-to-have and must not fail an upload. identifiers is the list FiveM's GetPlayerIdentifiers() returns; named keys (discord, steam, fivem, license, license2) are read too and win over the list. Only those five types are kept, ip never. They reach the player space only when the key that sent them is marked as staying on the game server (feeds_player_portal).

Request body

NameTypeDescription
fileRequired binary
filenameOptional string
metadataOptional string JSON object, passed through unchanged. At most 16 KB as compact JSON, 8 levels deep and 100 entries counted at every level; anything larger is refused with invalid_metadata, whose details name the limit, its maximum and the actual figure.
retentionExemptOptional boolean Spares this file from the retention policy.

Responses

  • 200OK
  • 401Something went wrong
  • 403Something went wrong
  • 413Something went wrong
  • 415Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/uploads/base64 Upload a base64 payload

For callers that cannot build a multipart body. PerformHttpRequest in Lua has no multipart support at all, so this is the only way a resource can upload without a helper script. Accepts a bare base64 string or a data URI.

Parameters

NameInDescription
Idempotency-KeyOptional header Returns the media the first call created instead of storing a second copy. PerformHttpRequest in Lua has no dependable timeout, so a retry after a lost response would otherwise be billed twice. Keys live for 24 hours and are scoped to the organisation and token.

Request body

NameTypeDescription
base64Required string
filenameOptional string Stored as original_name: its last path segment, without control characters, cut to 255 characters.
metadataOptional string JSON object, or a string holding one. Same bounds as the multipart metadata.

Responses

  • 200OK
  • 401Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/upload-tickets Mint a player upload ticket

Called by the game server with its own key, for one player's capture: lb-phone's GetPresignedUrl(source, uploadType) runs on the server with the player's source. The returned url is single use, accepts one kind, lives ten minutes and carries the player named here; the phone uploads to it without any key, and whatever the phone adds about the player is ignored. Requires the presigned-uploads feature and the upload scope of the kind.

Request body

NameTypeDescription
kindRequired string
playerOptional object The same shape as the player-metadata header. Named keys win over the identifiers list; malformed values are dropped, never refused.

Responses

  • 201Created
  • 401Something went wrong
  • 403Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/player-uploads/{ticket} Upload with a player ticket No key

No API key: the ticket is the credential. Answers the body /api/v1/uploads answers, so the phone reads data.url, without the X-Quota-* headers. Unknown, expired, used and revoked tickets all answer upload_ticket_invalid. A file of another kind than the ticket's is unsupported_media_type with details.expected_kind; any refusal gives the ticket back. Only the file and its name are read from the request: retentionExempt, metadata, Idempotency-Key and player-metadata are ignored, and the file is never deduplicated.

Parameters

NameInDescription
ticketRequired path

Request body

NameTypeDescription
fileRequired binary

Responses

  • 200OK
  • 401Something went wrong
  • 403Something went wrong
  • 413Something went wrong
  • 415Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.

Media

Reading and removing stored media.

GET /api/v1/media List media

Cursor paginated, not offset. On a table that grows continuously an offset page skips rows inserted between two requests, so a script walking the library silently misses uploads.

Requires the read-media scope and the api-listing feature.

Parameters

NameInDescription
kindOptional query
fromOptional query
toOptional query
per_pageOptional query
cursorOptional query

Responses

  • 200OK
  • 403Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
GET /api/v1/media/{media} Retrieve one media

Parameters

NameInDescription
mediaRequired path The ULID from the upload response.

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
PATCH /api/v1/media/{media} Update metadata or retention

Deliberately not a general patch. Everything else about a media describes bytes on disk, and letting an API rewrite the recorded size or checksum would make storage reconciliation report drift that does not exist.

Parameters

NameInDescription
mediaRequired path The ULID from the upload response.

Request body

NameTypeDescription
metadataOptional object Replaces the stored metadata, within the same bounds as on upload.
retention_exemptOptional boolean

Responses

  • 200OK
  • 404Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
DELETE /api/v1/media/{media} Delete a media

The URL stops resolving immediately. The file itself is removed after a seven day recycle window, so a mistake is recoverable.

Parameters

NameInDescription
mediaRequired path The ULID from the upload response.

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.

Introspection

What this instance is, and what your token can do.

GET /api/v1/status Instance health and identity No key

Unauthenticated on purpose. This is what an uptime probe and a status page call, and requiring a token would make the check fail for the wrong reason every time a key is rotated. It keeps answering during a maintenance window, when every other route may answer 503 maintenance.

Responses

  • 200OK
GET /api/v1/token Describe the presented token

The most useful endpoint when an integration misbehaves: it turns "uploads are failing" into "this key has no video scope" without anyone having to open a dashboard.

Responses

  • 200OK
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
GET /api/v1/usage Hourly usage for the organisation

Parameters

NameInDescription
daysOptional query

Responses

  • 200OK
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.

Assets

Files the server owns rather than a player: company logos, interface images, sounds. Grouped in categories, addressed by a readable key, versioned without limit, never purged by retention and not counted against quota.

Every route requires the manage-assets scope and the assets feature. Keep that token on the game server: a NUI that needs to upload gets a single-use upload ticket instead.

GET /api/v1/asset-categories List asset categories

Not paginated. Categories are few, and a server reads them once at boot.

Responses

  • 200OK
  • 403Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/asset-categories Create an asset category

The slug is permanent. It is a segment of every stable URL in the category, so it cannot be renamed later; the display name can.

Request body

NameTypeDescription
slugRequired string
nameRequired string
rulesOptional string

Responses

  • 201OK
  • 409Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
PATCH /api/v1/asset-categories/{category} Rename a category or change its rules

New rules apply to the next upload only. Versions already stored keep being served, so tightening a rule never takes an existing asset offline. Sending rules as an empty object clears every rule.

Parameters

NameInDescription
categoryRequired path

Request body

NameTypeDescription
nameOptional string
rulesOptional string

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
DELETE /api/v1/asset-categories/{category} Delete an empty category

Refused with asset_category_not_empty while the category holds assets, including deleted ones still in their seven day recycle window.

Parameters

NameInDescription
categoryRequired path

Responses

  • 200OK
  • 409Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
GET /api/v1/assets List assets

Cursor paginated, ordered by category then key.

Parameters

NameInDescription
categoryOptional query
per_pageOptional query
cursorOptional query

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
GET /api/v1/assets/{category}/{key} Retrieve an asset with its full version history

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/assets/{category}/{key} Upload a new version

Creates the asset on its first upload. Images are stored byte for byte and served at the stable URL immediately. Video and audio are converted to WebM or Ogg Opus when a NUI could not play them, and reach the stable URL only once that is done.

Uploading to a deleted asset brings it back with its history.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Request body

NameTypeDescription
fileRequired binary
metadataOptional string A JSON object, stored as sent, within the same bounds as a player upload.

Responses

  • 201Created
  • 404Something went wrong
  • 415Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
DELETE /api/v1/assets/{category}/{key} Delete an asset

The stable URL stops answering at once. The asset and every version go to a seven day recycle window, during which restore brings them back.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/assets/{category}/{key}/base64 Upload a new version as base64

For server scripts that cannot build a multipart body.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Request body

NameTypeDescription
base64Required string Bare base64 or a data URI.
filenameOptional string

Responses

  • 201Created
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/assets/{category}/{key}/rollback Serve another version at the stable URL

Nothing is copied or renumbered, so rolling forward again is the same call with a higher number.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Request body

NameTypeDescription
versionRequired integer

Responses

  • 200OK
  • 404Something went wrong
  • 409Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/assets/{category}/{key}/restore Restore a deleted asset

Brings back the versions deleted together with the asset. A version deleted on its own earlier stays deleted.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Responses

  • 200OK
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/assets/{category}/{key}/tickets Issue a single-use upload ticket

For uploads that start in a NUI. Anything inside a NUI runs on the player's machine, so the manage-assets token must never be shipped there. The game server checks the player is an admin, calls this, and hands upload_url to the NUI, which posts the file to it as multipart field file.

The ticket uploads to this one key, once, and the whole upload has to reach the server within 60 seconds of issue. The ticket is claimed only once the full request body has been received, so a large file must finish transferring inside that minute, not merely start.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.

Responses

  • 201Created
  • 404Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
POST /api/v1/asset-uploads/{ticket} Upload with a ticket No key

No API token: the ticket is the credential. Unknown, expired, used and revoked tickets all answer upload_ticket_invalid. A refused file (a broken category rule, say) gives the ticket back, so the admin can fix the file and resend within the same minute.

Parameters

NameInDescription
ticketRequired path

Request body

NameTypeDescription
fileRequired binary

Responses

  • 201Created
  • 401Something went wrong
  • 422Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.
DELETE /api/v1/assets/{category}/{key}/versions/{version} Delete one version

The only way history shrinks. The version currently served is refused with asset_version_current: roll back first.

Parameters

NameInDescription
categoryRequired path
keyRequired path Lowercase, such as lspd.logo.
versionRequired path

Responses

  • 200OK
  • 404Something went wrong
  • 409Something went wrong
  • 429Too many requests (rate_limited). Wait the seconds in Retry-After, then retry. error.details.retry_after_seconds is the limiter's own whole-second count, which can be up to a second short, so Retry-After is always one more than it and never below 1.
  • 503Temporarily unavailable. Two codes answer it: - maintenance, on every route but /api/v1/status, during a declared maintenance window. Wait the seconds in Retry-After, then send the same request again. error.details is MaintenanceDetails: retry_after_seconds (the same figure as the header), expected_end_at (the announced end, or null) and media_served (whether stored files and read requests keep being served). Retry-After and Cache-Control are sent with this code. - encoder_unavailable, on the routes that encode a file (uploads, asset versions, ticket redemption), when this instance cannot produce the requested format; error.details.format names it. Neither header is sent with this code.

Compatibility

Drop-in endpoints for existing scripts, such as screenshot-basic calls and older lb-phone releases, written against the v3 and v2 upload routes, the legacy /api/{image,video,audio} route or lb-upload. lb-phone 2.9.0 reaches none of them: it moves to its Custom method against /api/v1/uploads. Documented for migration, not for new integrations: use /api/v1/uploads.

POST /api/v3/file Compatible v3 upload

Field file. Answers { status, data: { id, url, originalUrl } } verbatim, with no meta block.

Request body

NameTypeDescription
fileRequired binary
filenameOptional string
metadataOptional string

Responses

  • 200OK
  • 429Too many requests. The body is the bare error string these clients already parse, with no delay in it: wait the seconds in Retry-After, then retry.
  • 503Temporarily unavailable: a maintenance window, or an encoder this instance lacks. The body is the bare error string these clients already parse, with no delay in it. During maintenance, wait the seconds in Retry-After, then retry; without that header the encoder is missing and retrying will not help.
POST /api/v3/file/base64 Compatible v3 upload, base64

Request body

NameTypeDescription
base64Required string
filenameOptional string

Responses

  • 200OK
  • 429Too many requests. The body is the bare error string these clients already parse, with no delay in it: wait the seconds in Retry-After, then retry.
  • 503Temporarily unavailable: a maintenance window, or an encoder this instance lacks. The body is the bare error string these clients already parse, with no delay in it. During maintenance, wait the seconds in Retry-After, then retry; without that header the encoder is missing and retrying will not help.
POST /api/v2/{kind} Compatible v2 upload

The form field is named after the kind: image, video or audio. The kind in the URL selects the field and nothing else. The stored kind still comes from sniffing the bytes, so a video posted to /image is stored as a video rather than mislabelled forever.

Parameters

NameInDescription
kindRequired path

Request body

NameTypeDescription
imageOptional binary
videoOptional binary
audioOptional binary

Responses

  • 200OK
  • 429Too many requests. The body is the bare error string these clients already parse, with no delay in it: wait the seconds in Retry-After, then retry.
  • 503Temporarily unavailable: a maintenance window, or an encoder this instance lacks. The body is the bare error string these clients already parse, with no delay in it. During maintenance, wait the seconds in Retry-After, then retry; without that header the encoder is missing and retrying will not help.
POST /api/{kind} Compatible legacy API upload

Undocumented everywhere now, and reconstructed from the shared/upload.lua of an older lb-phone release, which registered it as a legacy upload method; lb-phone 2.9.0 no longer does. Two traps: the audio form field is named recording, and url sits at the root of the response rather than under data.

Parameters

NameInDescription
kindRequired path

Request body

NameTypeDescription
imageOptional binary
videoOptional binary
recordingOptional binary The audio field. Not audio.

Responses

  • 200OK
  • 429Too many requests. The body is the bare error string these clients already parse, with no delay in it: wait the seconds in Retry-After, then retry.
  • 503Temporarily unavailable: a maintenance window, or an encoder this instance lacks. The body is the bare error string these clients already parse, with no delay in it. During maintenance, wait the seconds in Retry-After, then retry; without that header the encoder is missing and retrying will not help.
POST /lb-upload LBUpload

Field file. Answers { filename, link }, and the phone reads link.

Errors on this surface carry success: false, because that is what the LBUpload method of older lb-phone releases checks (2.9.0 no longer ships it). Returning the native envelope here would leave it looking for link, finding nothing, and telling the player "upload failed" with no reason attached.

Request body

NameTypeDescription
fileRequired binary

Responses

  • 200OK
  • 401Unauthorized
  • 429Too many requests, as success: false like every error on this route. The body has no delay in it: wait the seconds in Retry-After, then retry.
  • 503Temporarily unavailable, as success: false like every error on this route: a maintenance window, or an encoder this instance lacks. The body has no delay in it. During maintenance, wait the seconds in Retry-After, then retry; without that header the encoder is missing and retrying will not help.

Error codes

A refusal always carries a stable code, never translated, and error.docs, the link to its page. Branch your script on the code, never on the message.

The API key was missing, unknown, revoked, used from an address it does not allow, or lacks the permission for the call. Fixed on the key, or in the configuration that sends it.

The request itself was wrong: no file, a type or size that is not accepted, unreadable data, or something that does not exist. Fixed in the script that sends it.

The storage quota is full, or the key sent too many requests in a minute. Fixed by making room, raising the quota or slowing down.

The storage server did not answer, refused the file, or returned one that did not match. Fixed on the storage server.

Encoding the file failed, or something else went wrong on the platform side. Fixed by the platform operator.

HTTP Code What to do Retry
401 missing_tokenNo key sent Send the key in the Authorization header, with or without Bearer. For a script that cannot set a header, such as screenshot-basic, add ?apiKey= to the URL. No
401 invalid_tokenUnknown key The key sent does not exist. Copy it again in full, with no space or line break, from where you kept it when it was created. No
401 token_revokedRevoked key This key was revoked. Create a new one in the customer space and replace it in the resource that sends it. No
403 ip_not_allowedAddress not allowed The key is limited to some IP addresses. Add the address of the calling server to its list, or lift the restriction. No
403 insufficient_scopeKey lacks the scope The key lacks the scope this call needs, for instance upload-video to send a video. Add it, or use a key that has it. No
403 feature_not_enabledFeature not enabled This feature is not enabled for your organisation. The platform operator enables it: error.details.feature says which one to ask for. No
403 forbiddenAction not allowed Your account or your key is not allowed to do this in the organisation. Ask an owner for the permission. No
422 no_file_providedNo file in the request The request held no file. Send it in the file field of a multipart/form-data body, or as base64 to /api/v1/uploads/base64. No
415 unsupported_media_typeFile type not accepted This file type is not accepted. The type is read from the file's content, not from its extension: error.details.detected_mime says what was recognised. No
413 file_too_largeFile too large The file exceeds the size allowed for its type. error.details gives the size received and the maximum: shrink the file or compress it before sending. No
422 invalid_base64Unreadable base64 The base64 payload could not be decoded. Send bare base64, or a complete data URI such as data:image/png;base64,… No
422 invalid_metadataMetadata is not valid JSON The metadata field was not accepted. Send a valid JSON object, within the limits on size, depth and number of entries. No
422 corrupt_uploadUnreadable file The file could not be read as the type it claims to be: it is truncated or damaged. Send it again in full. No
413 quota_exceededQuota reached The organisation's storage quota is full. Free some space or ask for a larger quota: files already online keep being served. No
429 rate_limitedToo many requests Too many requests in a minute. Wait the number of seconds given by the Retry-After header, which every route sends, then try again. The native API also gives error.details.retry_after_seconds, the same wait rounded down (Retry-After is one second more), and error.details.limit, the number of requests allowed per minute. Yes
503 storage_unreachableStorage not answering The configured storage did not answer in time. If it is your FTP server, check that it is reachable, then try again. Yes
500 storage_write_failedStorage write failed The file could not be written to the storage. If it is your FTP server, check its free space and write permissions, then try again. Yes
500 storage_verification_failedStored file did not verify The file read back after writing did not match the one received, so it was deleted. Check your storage, then send the file again. No
404 media_not_foundMedia not found No media carries this identifier in your organisation, or it was deleted. Check the identifier the upload returned. No
404 route_not_foundUnknown API path No route answers this path. Check the URL and the API version against the reference. No
405 method_not_allowedWrong HTTP method This route exists, but not with this HTTP method. Use the one the reference gives. No
500 transcode_failedTranscode failed The file could not be re-encoded. Check that it plays correctly on your machine, then send it again. No
503 encoder_unavailableEncoder unavailable The encoder is not answering on this instance. The problem is on the platform's side: try again later. Yes
500 server_errorServer error Something unexpected went wrong on our side. Try again; if it comes back, give support the X-Request-Id header of the response. Yes
503 maintenanceMaintenance in progress The platform is under maintenance, and nothing needs changing on your side. Wait the number of seconds given by the Retry-After header, which every route sends, then send the same request again. The native API also gives error.details.retry_after_seconds (the same wait), error.details.expected_end_at (the announced end, or null) and error.details.media_served: when it is true, files already uploaded keep being served and read requests keep working, only uploads and changes wait. GET /api/v1/status keeps answering throughout. Yes
404 asset_not_foundAsset not found No asset carries this key in this category. Check the category and the key, in lowercase. No
404 asset_version_not_foundAsset version not found This asset has no version with this number. GET /api/v1/assets/{category}/{key} returns its full version history. No
404 asset_category_not_foundCategory not found No asset category carries this slug. Create it first, or check its spelling. No
409 asset_category_existsCategory already exists A category already carries this slug. Pick another one, or use the existing category. No
409 asset_category_not_emptyCategory still holds assets The category still holds assets. Delete them first: a deleted asset stays in the recycle bin for seven days, where it can still be restored, and keeps the category in use until it is erased. error.details.assets counts the live assets, error.details.trashed_assets those in the bin. No
422 asset_rule_violatedFile breaks its category's rules The file breaks a rule of its category. error.details names the rule and the expected value. No
409 asset_version_currentVersion currently served This version is the one the asset serves right now. Roll back to another version first, then delete this one. No
409 asset_version_not_readyVersion not ready This version is still processing, or its processing failed: it cannot be served. Wait for processing to finish, or pick another version. No
401 upload_ticket_invalidUpload ticket not valid The ticket is unknown, expired or already used. Ask your server for a new one: a ticket works once, within the 60 seconds after it was issued. No

Retry only the codes marked Yes; for a media upload, keep the same Idempotency-Key. The others will fail again for as long as the cause is there.