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.
https://aperture-app.preview.moncompanion.cloud
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>, forscreenshot-basiccalls 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
| Name | In | Description |
|---|---|---|
| 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
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| 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
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| ticketRequired | path |
Request body
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| kindOptional | query | |
| fromOptional | query | |
| toOptional | query | |
| per_pageOptional | query | |
| cursorOptional | query |
Responses
- 200OK
- 403Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames it. Neither header is sent with this code.
GET
/api/v1/media/{media}
Retrieve one media
Parameters
| Name | In | Description |
|---|---|---|
| mediaRequired | path | The ULID from the upload response. |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| mediaRequired | path | The ULID from the upload response. |
Request body
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| mediaRequired | path | The ULID from the upload response. |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames it. Neither header is sent with this code.
GET
/api/v1/usage
Hourly usage for the organisation
Parameters
| Name | In | Description |
|---|---|---|
| daysOptional | query |
Responses
- 200OK
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | Type | Description |
|---|---|---|
| slugRequired | string | |
| nameRequired | string | |
| rulesOptional | string |
Responses
- 201OK
- 409Something went wrong
- 422Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path |
Request body
| Name | Type | Description |
|---|---|---|
| nameOptional | string | |
| rulesOptional | string |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path |
Responses
- 200OK
- 409Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames it. Neither header is sent with this code.
GET
/api/v1/assets
List assets
Cursor paginated, ordered by category then key.
Parameters
| Name | In | Description |
|---|---|---|
| categoryOptional | query | |
| per_pageOptional | query | |
| cursorOptional | query |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames it. Neither header is sent with this code.
GET
/api/v1/assets/{category}/{key}
Retrieve an asset with its full version history
Parameters
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Request body
| Name | Type | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Request body
| Name | Type | Description |
|---|---|---|
| base64Required | string | Bare base64 or a data URI. |
| filenameOptional | string |
Responses
- 201Created
- 422Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Request body
| Name | Type | Description |
|---|---|---|
| versionRequired | integer |
Responses
- 200OK
- 404Something went wrong
- 409Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Responses
- 200OK
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| categoryRequired | path | |
| keyRequired | path | Lowercase, such as lspd.logo. |
Responses
- 201Created
- 404Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| ticketRequired | path |
Request body
| Name | Type | Description |
|---|---|---|
| fileRequired | binary |
Responses
- 201Created
- 401Something went wrong
- 422Something went wrong
- 429Too many requests (
rate_limited). Wait the seconds inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | In | Description |
|---|---|---|
| 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 inRetry-After, then retry.error.details.retry_after_secondsis the limiter's own whole-second count, which can be up to a second short, soRetry-Afteris 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 inRetry-After, then send the same request again.error.detailsisMaintenanceDetails:retry_after_seconds(the same figure as the header),expected_end_at(the announced end, or null) andmedia_served(whether stored files and read requests keep being served).Retry-AfterandCache-Controlare 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.formatnames 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
| Name | Type | Description |
|---|---|---|
| fileRequired | binary | |
| filenameOptional | string | |
| metadataOptional | string |
Responses
- 200OK
- 429Too many requests. The body is the bare
errorstring these clients already parse, with no delay in it: wait the seconds inRetry-After, then retry. - 503Temporarily unavailable: a maintenance window, or an encoder this
instance lacks. The body is the bare
errorstring these clients already parse, with no delay in it. During maintenance, wait the seconds inRetry-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
| Name | Type | Description |
|---|---|---|
| base64Required | string | |
| filenameOptional | string |
Responses
- 200OK
- 429Too many requests. The body is the bare
errorstring these clients already parse, with no delay in it: wait the seconds inRetry-After, then retry. - 503Temporarily unavailable: a maintenance window, or an encoder this
instance lacks. The body is the bare
errorstring these clients already parse, with no delay in it. During maintenance, wait the seconds inRetry-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
| Name | In | Description |
|---|---|---|
| kindRequired | path |
Request body
| Name | Type | Description |
|---|---|---|
| imageOptional | binary | |
| videoOptional | binary | |
| audioOptional | binary |
Responses
- 200OK
- 429Too many requests. The body is the bare
errorstring these clients already parse, with no delay in it: wait the seconds inRetry-After, then retry. - 503Temporarily unavailable: a maintenance window, or an encoder this
instance lacks. The body is the bare
errorstring these clients already parse, with no delay in it. During maintenance, wait the seconds inRetry-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
| Name | In | Description |
|---|---|---|
| kindRequired | path |
Request body
| Name | Type | Description |
|---|---|---|
| imageOptional | binary | |
| videoOptional | binary | |
| recordingOptional | binary | The audio field. Not audio. |
Responses
- 200OK
- 429Too many requests. The body is the bare
errorstring these clients already parse, with no delay in it: wait the seconds inRetry-After, then retry. - 503Temporarily unavailable: a maintenance window, or an encoder this
instance lacks. The body is the bare
errorstring these clients already parse, with no delay in it. During maintenance, wait the seconds inRetry-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
| Name | Type | Description |
|---|---|---|
| fileRequired | binary |
Responses
- 200OK
- 401Unauthorized
- 429Too many requests, as
success: falselike every error on this route. The body has no delay in it: wait the seconds inRetry-After, then retry. - 503Temporarily unavailable, as
success: falselike 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 inRetry-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.