SDK · Assets & URDF
The robot’s URDF and meshes, authenticated loading for three.js, and the completeness report of what the robot could and could not provide.
Generated from the SDK’s own source. The SDK Reference is the narrative; this page is every member, in the order the SDK declares them.
AssetsApi
A robot’s synced files, reachable as client.assets: its URDF, the meshes
and textures that URDF references, and the glue a three.js renderer needs
to fetch them with this client’s credentials.
list()
list(robotId: string): Promise<{ assets: ({ id: string; robot_id: string; kind: "urdf" | "mesh" | "texture"; name: string; media_type: string; size_bytes: number; sha256: string; created_at: string })[]; active_sync: { sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string } | null; urdf: { present: boolean; mesh_count: number; missing: ({ uri: string; element: "mesh" | "texture" })[] }; urdf_available: boolean | null; store: { bytes: number; used_bytes: number }; joint_state_slug: string | null }>
Every asset a robot has, plus whether its URDF is complete.
urdf.missing names the package:// URIs the sync could not resolve —
a count alone (“2 meshes missing”) sends a developer looking through a
workspace by hand, the URIs are what they can act on.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
Returns Promise<{ assets: ({ id: string; robot_id: string; kind: "urdf" | "mesh" | "texture"; name: string; media_type: string; size_bytes: number; sha256: string; created_at: string })[]; active_sync: { sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string } | null; urdf: { present: boolean; mesh_count: number; missing: ({ uri: string; element: "mesh" | "texture" })[] }; urdf_available: boolean | null; store: { bytes: number; used_bytes: number }; joint_state_slug: string | null }>.
syncStatus()
syncStatus(robotId: string, syncId: string): Promise<{ sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string }>
The status of one sync by id — for reconnecting to a sync already in flight, not for starting one.
Starting a sync stays out of this SDK, on purpose. Assets are
transferred only on a developer’s explicit request from the console —
an Owner-tier action, done once. This method is a different thing:
list()'s
active_sync (or a busy refusal’s assetSyncBusyDetails) hands a
caller a sync_id for a sync that is already running, and before
this method existed there was no way for anything built on this SDK to
do anything with that id except throw it away, even though the sync was
readable server-side the whole time.
A page reload is the case this exists for: whatever held the sync_id
in memory is gone, list() (or a fresh busy refusal) hands it back,
and this is how a caller resumes watching the same sync instead of
either losing the progress bar or being told to start a second one.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
|
syncId |
string |
yes |
Returns Promise<{ sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string }>.
get()
get(robotId: string, assetId: string): Promise<AssetBytes>
One asset’s bytes by id — a mesh, or any asset directly, addressed the same way createMeshLoader reaches one internally.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
|
assetId |
string |
yes |
Returns Promise<AssetBytes>.
References: AssetBytes
urdf()
urdf(robotId: string): Promise<string>
The robot’s URDF, with every package:// mesh URI already rewritten to
an absolute Fleetless asset URL — ready to hand straight to
URDFLoader.parse(xml). Decoded as UTF-8 text rather than left as
bytes because every consumer needs it as a string for exactly that call.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
Returns Promise<string>.
createMeshLoader()
createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate
The mesh callback for urdf-loader: an <img> tag and the default
three.js loaders cannot set an Authorization header, and the platform
deliberately has no signed URLs and no token in the query string, so
every app would otherwise write this glue itself, and each one
differently.
Returns a function with loadMeshCb’s own signature — assign it
directly:
loader.loadMeshCb = client.assets.createMeshLoader(robotId, loader.defaultMeshLoader.bind(loader))
delegate does the actual format-specific parsing (STL/OBJ/DAE/GLTF —
loader.defaultMeshLoader already knows how); this method’s own job is
only what a plain loader cannot do: fetch path with the
Authorization header, and hand the delegate something it can load
without one. It does that by fetching the bytes itself, wrapping them
in a Blob, and calling delegate with an object URL substituted for
path — so the delegate never touches the network, and the object URL
is revoked the moment delegate reports success or failure (or the
timeout elapses), never left for the caller to remember.
Refuses, via onComplete(null, err), if the manager it is handed
already has prepareUrdfScene’s URL modifier installed — the two
read different URDF sources and combining them on one manager breaks
one half or the other, never obviously (see prepareUrdfScene’s own
doc comment). Checked at the point the mistake would actually manifest
rather than left to a paragraph a developer might not read.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
|
delegate |
MeshLoaderDelegate |
yes |
|
options |
CreateMeshLoaderOptions |
no |
Returns MeshLoaderDelegate.
References: MeshLoaderDelegate, CreateMeshLoaderOptions
prepareUrdfScene()
prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>
Authenticated loading for everything three.js fetches to render a
textured robot — not only meshes. Installs
manager.setURLModifier so every load manager oversees resolves
to a pre-fetched blob: URL: a top-level <mesh>, a <material>'s
<texture>, and an image a .dae references internally via
<init_from> — three cases, one mechanism, because the browser never
fetches an asset directly. Every network read goes through this SDK
with the bearer token first; a blob: URL is document-local and dies
with the page, so nothing about who may read what changes.
const manager = new THREE.LoadingManager()
const { urdfText, missing, dispose } = await client.assets.prepareUrdfScene(robotId, manager)
const loader = new URDFLoader(manager)
const robot = loader.parse(urdfText)
scene.add(robot)
// later, once the scene has finished loading (or on unmount):
dispose()
Do not also install createMeshLoader on the same manager. The two
consume different URDF sources — this method fetches the URDF’s raw
bytes, createMeshLoader is meant to pair with urdf()'s
cloud-rewritten text. Not a double-fetch.
What actually happens is asymmetric breakage,
whichever URDF text the combination ends up parsing: paired with
this method’s raw text, createMeshLoader receives urdf-loader’s
resolvePath() output (/pkg/rel) rather than an absolute Fleetless
URL and 404s every mesh, while textures still resolve; paired with
urdf()'s rewritten text instead, meshes load and every texture 401s
— createMeshLoader bypasses this method’s URL modifier entirely for
meshes (that’s what loadMeshCb means), so there is no hook left for
a texture. Either way a developer who combined them by accident would
debug the wrong symptom, which is worse than the original (already
wrong) warning being merely unhelpful.
Enforced, not only documented. createMeshLoader’s returned
callback checks whether the manager it is handed already has this
method’s URL modifier installed and fails loudly via
onComplete(null, err) before ever touching the network, rather than
relying on a developer having read this paragraph.
Why raw bytes, not urdf()'s rewritten text. Both urdf-loader’s
default mesh loading and ColladaLoader compute the base path they use
to resolve a .dae’s internal references (LoaderUtils.extractUrlBase)
from the URL they were originally asked to load — before
manager.resolveURL()/the URL modifier ever runs; the modifier only
changes what bytes get fetched, never what further relative references
resolve against. Feeding it urdf()'s already-rewritten
.../assets/<uuid> text would compute a base of .../assets/, and
textures/skin.png joined onto that would never match anything this
method’s map knows about. Raw package:// text is what keeps the two
in sync.
urdf-loader resolves package:// itself, before any of this runs —
a second resolution stage this method has to account for.
URDFLoader.parse()'s own resolvePath()
rewrites package://pkg/rel using this.packages (default '') to
/pkg/rel — a root-relative URL — and that is what reaches
loadMeshCb/ColladaLoader/manager.resolveURL(), not the original
string. So the modifier is registered under two keys per asset: the
literal package:// name (asset.name, for a caller who sets
loader.packages = (pkg) => \package://${pkg}`to reconstruct it, or a renderer that never went throughresolvePath()at all) and the root-relative formurdf-loader's own *default* packages: ‘’ produces (/pkg/rel) — covering the common case with zero required caller configuration. A .dae's internal <init_from>ref resolves the same way one level deeper:ColladaLoadercomputes its own working path from *its*urlargument (already/pkg/relby the time it gets there under the default), so the internal reference lands on/pkg/textures/skin.png— exactly the second key, derived the same way. A caller whoseloader.packages` does something else entirely
(a custom map, not the default and not the reconstruction above) is
outside what this method can predict — see the ownership rule below
for what happens to that reference.
This method only claims what it owns — not every unmapped
reference. manager is frequently the
caller’s own scene-wide LoadingManager, shared for an HDRI, an
environment map, a font atlas, a ground texture — none of which have
anything to do with this robot. Refusing everything unmapped would
silently empty every one of those the moment a caller shares their
manager. So the rule is narrower: a package://
reference, or a root-relative path whose leading segment names a ROS
package this robot’s assets (or missing) actually mention, is this
method’s to resolve or refuse; an unmapped one falls back to the
normalized form (below) and then to a shared, inert, page-local
blob: URL — never the original string, so a hostile URDF naming an
unsynced or off-namespace reference still cannot make three.js touch
the network for it. Anything else — not in that namespace — is left
completely alone, except an absolute http(s) URL, which is
refused regardless of namespace: the one case this method cannot leave
ambiguous, because a hostile URDF naming an attacker’s host directly
(bypassing package:// entirely) is exactly what this rule exists to
close, and three.js would otherwise fetch it for real, off-origin, the moment
the direct and namespace checks both miss.
A .dae’s own internal reference gets a second-chance, normalized
lookup, reproduced in a real browser. three.js
builds the request for one by plain string concatenation — no ../.
collapsing — while asset.name carries the normalized tail
(@fleetless/contracts’ naming rule). So ../textures/skin.png or
./textures/skin.png, both ordinary exporter output, would otherwise
miss the direct key even though the reference is entirely resolvable.
Verified independently that the top-level <mesh>/<texture> case
never needs this: asset.name there is the URDF’s own package:// URI
verbatim and resolvePath() rewrites it by the same unnormalized
concatenation on both sides, so the direct key already matches.
dispose() does not undo any of this. See its own doc comment on
UrdfSceneResources.
Pre-fetch is unavoidable, not merely a choice: a URL modifier
cannot be asynchronous, so every asset it might be asked for has to
already be a blob: URL before URDFLoader.parse runs. Bounded by
options.concurrency (default 6) and scoped to only kind: 'mesh' and
kind: 'texture' assets — which is already “what the URDF references”,
since a re-sync reconciles and assets the current URDF no longer
references stop belonging to the robot. Not an unbounded fetch of
everything the robot has ever had.
| Parameter | Type | Required | Description |
|---|---|---|---|
robotId |
string |
yes |
|
manager |
UrdfSceneManager |
yes |
|
options |
PrepareUrdfSceneOptions |
no |
Returns Promise<UrdfSceneResources>.
References: UrdfSceneManager, PrepareUrdfSceneOptions, UrdfSceneResources
AssetBytes
An asset’s bytes plus its declared media type — the shape assets.get answers with.
| Property | Type | Required | Description |
|---|---|---|---|
body |
Uint8Array |
yes |
The asset’s raw bytes, exactly as stored. |
mime |
string | null |
yes |
The declared media type, or null if the store did not record one. |
MeshLoaderDelegate
The signature urdf-loader’s own loadMeshCb uses (verified against that
library’s docs, not guessed): manager/material pass straight through
from whatever called this, and onComplete is how a mesh loader reports
success or failure — it never throws.
This SDK does not depend on urdf-loader or three.js — manager,
material and the resolved object are all opaque here, exactly what makes
createMeshLoader usable from any renderer that speaks this same shape,
not only that one library.
type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void
CreateMeshLoaderOptions
What assets.createMeshLoader accepts beyond the robot and the delegate.
| Property | Type | Required | Description |
|---|---|---|---|
timeoutMs |
number |
no |
How long to wait for delegate’s onComplete before giving up. Defaults to 30s. A URDF pulls in thirty-odd meshes and a dashboard using this callback can run for days — a delegate that never calls back (an exception three.js swallowed internally, a parser stuck on a malformed mesh) must not leak the object URL or hang the load forever. |
UrdfSceneManager
three.js’s own LoadingManager.setURLModifier(callback) shape — the
only thing assets.prepareUrdfScene needs from a three.js
LoadingManager, so a caller passes theirs straight in.
Every load the manager oversees is routed through callback first — not
only the loader you handed the manager to, but every loader it constructs
internally on the same manager (ColladaLoader’s own TextureLoader for
a .dae’s <init_from> images, in particular). That is the one hook
that exists one level up, for everything, where createMeshLoader’s
per-loader loadMeshCb override does not reach: TextureLoader has no
override hook of its own.
Structural, not import('three') — this SDK does not depend on three.js
or urdf-loader, same discipline as MeshLoaderDelegate above.
setURLModifier()
setURLModifier(callback: (url: string) => string): unknown
Installs a callback every load through this manager is routed through first.
| Parameter | Type | Required | Description |
|---|---|---|---|
callback |
(url: string) => string |
yes |
Returns unknown.
PrepareUrdfSceneOptions
What assets.prepareUrdfScene accepts beyond the robot and the manager.
| Property | Type | Required | Description |
|---|---|---|---|
concurrency |
number |
no |
How many assets to fetch in parallel. Default 6 — a default that keeps memory and connection counts sane for the common case (dozens of meshes), not a number with a sweep behind it. Pass your own if you have a reason to. Must be a positive integer — 0 or negative rejects with invalid_option rather than silently fetching nothing and returning a scene that renders completely blank with no error to explain why. |
signal |
AbortSignal |
no |
Cancels this call — a caller who navigates away or switches to a different robot mid-load can abort every in-flight fetch this method has started, not merely stop it from starting new ones. Checked before the first request; if it fires while a request is already in progress, the SDK forwards it straight to fetch(), so the connection itself is torn down, not just abandoned by this SDK while it keeps running in the background. Every blob: URL already created before the abort is revoked before this call rejects with FleetlessError('aborted', ...) — the same guarantee a load that fails outright already had: an aborted load must not leak what it had already fetched. manager’s URL modifier is only ever installed once every asset has resolved (success or the throw below) — an aborted call never installs a partial one, so manager is left exactly as it was if this rejects before that point. |
UrdfSceneResources
What assets.prepareUrdfScene resolves with: the URDF text to parse, what
the sync could not resolve, and the cleanup for everything it fetched.
| Property | Type | Required | Description |
|---|---|---|---|
urdfText |
string |
yes |
The robot’s URDF as raw text — package:// URIs intact, not rewritten to asset URLs. Hand it straight to URDFLoader.parse(urdfText) once setURLModifier is installed (this method already installed it on manager before returning). |
missing |
({ uri: string; element: "mesh" | "texture" })[] |
yes |
The same entries assets.list()'s urdf.missing reports — verbatim, not reduced to bare strings. Each carries element ('mesh' | 'texture') alongside uri: before this, both kinds arrived as an undifferentiated string[] and a caller could only ever say “N meshes missing”, wrongly, for a URDF whose gap was actually a texture. Reducing this field back to string[] here would throw the distinction away again at exactly the point a caller would render it. If you only need the URIs, missing.map(m => m.uri). Top-level only — not a .dae’s internal references. The cloud builds this list from the URDF text alone (<mesh>/<texture> filename attributes), which is the only place it can see without parsing every .dae a sync touches; it never has and never can include an internal <init_from> reference. Do not build a completeness check on it as though it covered both: a .dae-internal reference the sync could not resolve surfaces through the sync’s own failure reporting instead, not here. Not thrown either way: an incomplete URDF still renders what it has (urdfCompleteness’s own contract), so the caller decides whether to warn, block, or ignore before calling URDFLoader.parse(urdfText). A reference NOT in this list that still fails to load at render time is a different failure — the store answered but the fetch itself did not. |
dispose()
dispose(): void
Revokes every object URL this call created that carries bytes. Call once the scene has finished loading (success or failure) or on unmount — safe to call more than once.
One object URL is deliberately kept: the shared zero-byte placeholder every refused reference resolves to. It costs nothing to leave alive, and leaving it is what lets this method stay simple — the installed URL modifier goes on refusing an owned-but-gone reference correctly, rather than falling back to the original string once the map is empty.
Does not touch manager’s URL modifier. Resetting it to the
identity function here would reopen the very hole the modifier exists to
close, the moment the same manager was used again — for a second robot,
or for anything else — until a later prepareUrdfScene call happened to
overwrite it. The installed modifier is left running,
and with this call’s map now empty it already refuses anything it would
have owned and passes through anything it would not have, correctly,
on its own.
Returns void.
AssetListResponse
The asset-list-response wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.
Asset
The asset wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.
UrdfCompleteness
The urdf-completeness wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.