fleetlessfleetlessdocs
Reference/API/Schemas

API Schemas

Every request body, query string and response the API Routes page refers to, field by field, generated from the same schemas the platform validates with. A nested object’s fields follow it with a dotted path; [] marks the items of an array, and * each entry of a map whose keys you choose yourself. An empty description is a gap in the contracts, counted and not allowed to grow.

accept-team-invite-request

Field Type Required Description
token string yes The opaque invitation token from the link. Unknown, expired and already-accepted all collapse into 410 token_spent — telling them apart would say whether a token ever existed.
display_name string | null no An optional name, overriding whatever the invitation pre-filled. Absent keeps it.

alert-list-response

Field Type Required Description
alerts object[] yes
alerts[].id string yes
alerts[].robot_id string yes
alerts[].slug string yes
alerts[].name string yes
alerts[].enabled boolean yes
alerts[].severity "warning" | "error" yes
alerts[].condition object yes
alerts[].condition.kind "above" yes
alerts[].condition.threshold number yes
alerts[].condition.resolve_hysteresis number yes
alerts[].condition.kind "below" yes
alerts[].condition.threshold number yes
alerts[].condition.resolve_hysteresis number yes
alerts[].condition.kind "equals" yes
alerts[].condition.value number | string | boolean yes
alerts[].state "ok" | "firing" yes
alerts[].state_since string | null yes
alerts[].last_value unknown | null yes
alerts[].created_at string yes

app

Field Type Required Description
id string yes The app in the API, assigned by the cloud and stable for the life of the app. Everything app-scoped takes this as its :id.
org_id string yes The organisation that owns this app. Every developer route is already scoped to the caller’s org, so this confirms what a client is looking at rather than being a filter it applies.
name string yes The display name, shown in the console and available to the developer’s own pages through the app.name mail-template variable. Free text, changed through PATCH /api/apps/:id.
identifier string yes The stable handle a client sends at login, lowercase and underscore-separated. Globally unique, not per organisation — clientLoginRequest carries no org context, so a collision is refused with identifier_taken.
robot_ids string[] yes The robots this app may reach, each referenced individually. Tags never grant rights, and a robot absent from this list is invisible to the app whatever a role grants.
default_role_id string | null yes The role an app user gets when created or invited without an explicit one. null means this app has not chosen a default, the normal state of an app created before its roles were configured — and then a create or invite that omits role_id gets validation_error, not a user with no role. An invitation resolves the role when issued, so changing this never re-aims an outstanding one. The role must belong to this app, which PATCH /api/apps/:id checks and the schema cannot.
created_at string yes When the app was created, as an ISO 8601 timestamp. GET /api/apps orders by this field.

app-auth-config

Field Type Required Description
self_registration boolean yes Whether a stranger may create an account in this app. Off refuses POST /api/client/register with 403 registration_closed, and refuses an unknown identity at an OIDC callback with the same reasoning — one switch for one decision, whichever door the person arrives at.
allowed_domains string[] yes The email domains self-registration accepts, lowercase. An empty list means no domain restriction, not “nobody” — the switch above is what closes the door. An invitation always bypasses this, by password and through a provider alike.
allowed_origins string[] yes The origins the client auth API answers CORS for, and the only origins an OIDC redirect_uri may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match.
mcp_enabled boolean yes Whether this app serves an MCP endpoint at /mcp/<identifier>. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token.
invite_url string | null yes The page in the developer’s app that accepts an invitation, with {token} where the token goes. null means the hosted page in hosted_pages is used.
verify_url string | null yes The page that confirms a new address, with {token} where the token goes. null means the hosted page in hosted_pages is used.
reset_url string | null yes The page that takes a new password, with {token} where the token goes. null means the hosted page in hosted_pages is used.
mcp_login_url string | null yes The page an MCP authorization redirects to, with {interaction} where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it. null means the hosted MCP sign-in in hosted_pages is used.
app_url string | null yes The app’s own home page, linked as Open <app> when a hosted flow is done. null makes the hosted done page say You can close this tab.
sign_in_methods object yes Which sign-in methods the app offers: password, emailed code, or both — at least one. Identity providers stay on top of either. The default is password only.
sign_in_methods.password boolean yes Whether app users may sign in with a password. Off refuses POST /api/client/login with method_not_allowed, and registration and invitations then take no password.
sign_in_methods.email_code boolean yes Whether app users may sign in with a six-digit code mailed to them, valid ten minutes. A code needs no URL, so it works in local development and in an app with no web UI.
two_factor "off" | "optional" | "required" yes Whether the app asks for an authenticator code: off (the default), optional or required. A person with a confirmed authenticator is asked at every sign-in whatever the policy; a sign-in through an identity provider is never asked.
hosted_logo_url string | null yes Where the hosted pages load the app’s logo from, <portal>/app/<identifier>/logo, or null when no logo is stored. Read-only — the logo is written through PUT /api/apps/:id/auth-config/logo.
hosted_accent string | null yes The accent colour of the hosted pages, #rrggbb in lowercase, or null for the neutral shell’s own.
hosted_pages object yes The Fleetless-hosted pages an unset URL falls back to, as templates. Read-only: minted by the cloud from the auth portal and the app’s identifier.
hosted_pages.invite_url string yes The hosted invitation page, <portal>/app/<identifier>/invite/{token}.
hosted_pages.verify_url string yes The hosted email-confirmation page, <portal>/app/<identifier>/verify/{token}.
hosted_pages.reset_url string yes The hosted new-password page, <portal>/app/<identifier>/reset/{token}.
hosted_pages.mcp_login_url string yes The hosted MCP sign-in, <portal>/app/<identifier>/mcp/{interaction}.
oidc_callback_url string yes The one callback URL to register at every identity provider, the same for every app and every provider. Read-only — it is minted by the cloud from its own public base URL, and a writable version of this field would let a caller point the return leg, which carries an authorization code, at a host they own.
updated_at string yes When the configuration was last written, as an ISO 8601 timestamp.

app-deletion-summary

Field Type Required Description
user_count integer yes App users deleted with the app. They are the developer’s own customers, not Fleetless users, and exist in no other app.
role_count integer yes Roles deleted with the app, each with its per-robot slug grants.
server_key_count integer yes Server keys deleted with the app. A client still holding one is refused at its next request.
invitation_count integer yes Outstanding invitations — unspent and unexpired — that will never be accepted.
oidc_provider_count integer yes Identity providers configured for this app. The providers themselves are somebody else’s; only this app’s configuration of them goes.
mail_template_count integer yes Custom mail templates, of at most three. A kind using the Fleetless default text is not counted — there is no row to lose.

app-invitation

Field Type Required Description
id string yes The invitation, as listed and revoked by the developer.
app_id string yes The app the invitee will belong to.
email string yes The address the invitation was addressed to.
role_id string yes The role the invitee holds once they accept. Resolved at creation, so a later change to the app’s default role does not silently re-aim an outstanding invitation.
expires_at string yes When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does.
accept_url string yes The link to give the invitee: the app’s invite_url with the token substituted for {token}, or the Fleetless-hosted invitation page when the app has configured none. Bounded like every other URL that gets mailed, logged and rendered.
mail "sent" | "not_requested" | "not_configured" | "failed" yes What happened to the mail: sent means the SMTP server accepted it, not that it was delivered; not_requested means none was attempted because the caller asked for none; not_configured is an expected state and not a failure; failed is the one worth somebody’s attention.

app-invitation-list-response

Field Type Required Description
invitations object[] yes The app’s outstanding invitations, without their tokens. An accepted one is history and does not appear.
invitations[].id string yes The invitation, as listed and revoked by the developer.
invitations[].app_id string yes The app the invitee will belong to.
invitations[].email string yes The address the invitation was addressed to.
invitations[].role_id string yes The role the invitee holds once they accept. Resolved at creation, so a later change to the app’s default role does not silently re-aim an outstanding invitation.
invitations[].expires_at string yes When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does.

app-list-response

Field Type Required Description
apps object[] yes Every app of the caller’s organisation, oldest first by created_at. The org scope is the whole filter — there is no id to narrow by and nothing to refuse.
apps[].id string yes The app in the API, assigned by the cloud and stable for the life of the app. Everything app-scoped takes this as its :id.
apps[].org_id string yes The organisation that owns this app. Every developer route is already scoped to the caller’s org, so this confirms what a client is looking at rather than being a filter it applies.
apps[].name string yes The display name, shown in the console and available to the developer’s own pages through the app.name mail-template variable. Free text, changed through PATCH /api/apps/:id.
apps[].identifier string yes The stable handle a client sends at login, lowercase and underscore-separated. Globally unique, not per organisation — clientLoginRequest carries no org context, so a collision is refused with identifier_taken.
apps[].robot_ids string[] yes The robots this app may reach, each referenced individually. Tags never grant rights, and a robot absent from this list is invisible to the app whatever a role grants.
apps[].default_role_id string | null yes The role an app user gets when created or invited without an explicit one. null means this app has not chosen a default, the normal state of an app created before its roles were configured — and then a create or invite that omits role_id gets validation_error, not a user with no role. An invitation resolves the role when issued, so changing this never re-aims an outstanding one. The role must belong to this app, which PATCH /api/apps/:id checks and the schema cannot.
apps[].created_at string yes When the app was created, as an ISO 8601 timestamp. GET /api/apps orders by this field.

app-mail-template

Field Type Required Description
kind "invite" | "verify" | "reset" | "login_code" yes Which of the four mails this template replaces.
subject string yes The subject line, a Liquid template. Bounded because a subject is rendered into a header.
text string yes The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML.
html string | null yes The optional HTML body, a Liquid template. null means this template is text-only, which is a complete mail and not a half-configured one.
updated_at string yes When the template was last written, as an ISO 8601 timestamp.

app-mail-template-list-response

Field Type Required Description
templates object[] yes The app’s custom templates. A kind that does not appear is one using the Fleetless default text — an ordinary state, not a missing row.
templates[].kind "invite" | "verify" | "reset" | "login_code" yes Which of the four mails this template replaces.
templates[].subject string yes The subject line, a Liquid template. Bounded because a subject is rendered into a header.
templates[].text string yes The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML.
templates[].html string | null yes The optional HTML body, a Liquid template. null means this template is text-only, which is a complete mail and not a half-configured one.
templates[].updated_at string yes When the template was last written, as an ISO 8601 timestamp.

app-oidc-provider

Field Type Required Description
id string yes The provider row, as listed, patched and deleted by the developer.
app_id string yes The app this provider signs users in to.
slug string yes The stable handle in the sign-in URL (/api/client/oidc/:slug/start) and on the developer’s own button. Unique per app, lowercase and hyphen-separated — not the underscore-separated grammar identifier uses. Immutable: linked identities are keyed by it.
name string yes What the developer’s sign-in page calls this provider, e.g. “Sign in with Azure AD”. Free text; the only field of this shape the public GET /api/client/providers reveals besides the slug.
issuer string yes The provider’s issuer URL, from which discovery and the JWKS are read. http(s) only, with no credentials, query or fragment — RFC 8414 §3 builds the discovery URL from the issuer’s path, so a query there is meaningless and an @ is a redirect trick. This is not the SSRF defence: it cannot tell a loopback dev provider from a loopback database, and the real check refuses loopback, link-local and private ranges at the fetch.
client_id string yes The OAuth client the developer registered at their provider for Fleetless.
scopes string[] yes The scopes requested at the provider. At least one — a request that asks for nothing learns nothing — and bounded, because an unbounded array on a stored, logged and rendered shape is a size nobody chose.
link_verified_emails boolean yes Whether a federated login may join an existing app user with the same address. It needs the provider to assert email_verified as well: either condition alone is account takeover, since a provider that lets anyone type any address into a profile would otherwise hand over every matching account, and a developer who connects a provider for a subset of their users would otherwise silently merge strangers.
enabled boolean yes Whether this provider is offered. A disabled provider disappears from GET /api/client/providers and refuses a start with provider_disabled, without the row and its linked identities being deleted.
created_at string yes When the provider was configured, as an ISO 8601 timestamp.

app-oidc-provider-list-response

Field Type Required Description
providers object[] yes Every provider configured on this app, disabled ones included — this is the developer’s management view, unlike the public GET /api/client/providers, which lists only what a user can actually press.
providers[].id string yes The provider row, as listed, patched and deleted by the developer.
providers[].app_id string yes The app this provider signs users in to.
providers[].slug string yes The stable handle in the sign-in URL (/api/client/oidc/:slug/start) and on the developer’s own button. Unique per app, lowercase and hyphen-separated — not the underscore-separated grammar identifier uses. Immutable: linked identities are keyed by it.
providers[].name string yes What the developer’s sign-in page calls this provider, e.g. “Sign in with Azure AD”. Free text; the only field of this shape the public GET /api/client/providers reveals besides the slug.
providers[].issuer string yes The provider’s issuer URL, from which discovery and the JWKS are read. http(s) only, with no credentials, query or fragment — RFC 8414 §3 builds the discovery URL from the issuer’s path, so a query there is meaningless and an @ is a redirect trick. This is not the SSRF defence: it cannot tell a loopback dev provider from a loopback database, and the real check refuses loopback, link-local and private ranges at the fetch.
providers[].client_id string yes The OAuth client the developer registered at their provider for Fleetless.
providers[].scopes string[] yes The scopes requested at the provider. At least one — a request that asks for nothing learns nothing — and bounded, because an unbounded array on a stored, logged and rendered shape is a size nobody chose.
providers[].link_verified_emails boolean yes Whether a federated login may join an existing app user with the same address. It needs the provider to assert email_verified as well: either condition alone is account takeover, since a provider that lets anyone type any address into a profile would otherwise hand over every matching account, and a developer who connects a provider for a subset of their users would otherwise silently merge strangers.
providers[].enabled boolean yes Whether this provider is offered. A disabled provider disappears from GET /api/client/providers and refuses a start with provider_disabled, without the row and its linked identities being deleted.
providers[].created_at string yes When the provider was configured, as an ISO 8601 timestamp.

app-user

Field Type Required Description
id string yes The app user in the API, assigned by the cloud and stable for the life of the account.
app_id string yes The app this user belongs to, and the whole of their scope. An app user of one app is nobody at another, even inside the same organisation.
email string yes The address the account is identified by. Unique per app, case-insensitively — the same address may exist as an unrelated account in another app of the same organisation.
display_name string | null yes Optional human name, shown by the developer’s own UI instead of the address where present. null when the user never supplied one; never used for authentication.
role_id string yes The role that decides what this user may reach. Roles are the only visibility filter: what a role does not grant does not exist for that user.
status "pending_verification" | "active" | "blocked" yes Where the account is in its lifecycle. Only active may log in; pending_verification and blocked are both refused with the same invalid_credentials a wrong password gets, so a failed login is not an account-enumeration oracle.
has_password boolean yes Whether this account has a Fleetless-held password. false is an identity-provider-only account, or an invitation not yet accepted — it does not mean blocked and it does not mean without access. No hash, no algorithm and no “last changed” travels here, and nothing on the wire can say whether a password is strong or already known to somebody else.
providers string[] yes The slugs of the identity providers this account is linked to, empty for a password-only user. Lets a developer’s user list say where an account came from without a second request.
last_login_at string | null yes When this user last signed in, or null if they never have. Required and nullable rather than optional, so never logged in stays distinguishable from this field was not loaded.
two_factor object yes The account’s second factor, as a developer’s user list shows it. No secret and no code travels here; resetting it is DELETE /api/apps/:id/users/:userId/two-factor.
two_factor.enabled boolean yes Whether the account has a confirmed authenticator app (TOTP). When it has, every sign-in that yields a session asks for a code as well — whatever the app’s policy — except a sign-in through an identity provider, which owns that sign-in.
two_factor.enabled_at string | null yes When the authenticator was confirmed, or null while enabled is false.
two_factor.recovery_codes_left integer yes How many of the ten single-use recovery codes are still unspent. 0 while enabled is false.
created_at string yes When the account was created, as an ISO 8601 timestamp.

app-user-list-response

Field Type Required Description
users object[] yes Every user of this app. An app with no users answers an empty array, not an absent key.
users[].id string yes The app user in the API, assigned by the cloud and stable for the life of the account.
users[].app_id string yes The app this user belongs to, and the whole of their scope. An app user of one app is nobody at another, even inside the same organisation.
users[].email string yes The address the account is identified by. Unique per app, case-insensitively — the same address may exist as an unrelated account in another app of the same organisation.
users[].display_name string | null yes Optional human name, shown by the developer’s own UI instead of the address where present. null when the user never supplied one; never used for authentication.
users[].role_id string yes The role that decides what this user may reach. Roles are the only visibility filter: what a role does not grant does not exist for that user.
users[].status "pending_verification" | "active" | "blocked" yes Where the account is in its lifecycle. Only active may log in; pending_verification and blocked are both refused with the same invalid_credentials a wrong password gets, so a failed login is not an account-enumeration oracle.
users[].has_password boolean yes Whether this account has a Fleetless-held password. false is an identity-provider-only account, or an invitation not yet accepted — it does not mean blocked and it does not mean without access. No hash, no algorithm and no “last changed” travels here, and nothing on the wire can say whether a password is strong or already known to somebody else.
users[].providers string[] yes The slugs of the identity providers this account is linked to, empty for a password-only user. Lets a developer’s user list say where an account came from without a second request.
users[].last_login_at string | null yes When this user last signed in, or null if they never have. Required and nullable rather than optional, so never logged in stays distinguishable from this field was not loaded.
users[].two_factor object yes The account’s second factor, as a developer’s user list shows it. No secret and no code travels here; resetting it is DELETE /api/apps/:id/users/:userId/two-factor.
users[].two_factor.enabled boolean yes Whether the account has a confirmed authenticator app (TOTP). When it has, every sign-in that yields a session asks for a code as well — whatever the app’s policy — except a sign-in through an identity provider, which owns that sign-in.
users[].two_factor.enabled_at string | null yes When the authenticator was confirmed, or null while enabled is false.
users[].two_factor.recovery_codes_left integer yes How many of the ten single-use recovery codes are still unspent. 0 while enabled is false.
users[].created_at string yes When the account was created, as an ISO 8601 timestamp.

asset

Field Type Required Description
id string yes The asset’s id in the store.
robot_id string yes The robot this asset belongs to.
kind "urdf" | "mesh" | "texture" yes What the file is: the urdf itself, a mesh it references, or a texture a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch.
name string yes What the robot called it — for a mesh, the package:// URI the URDF references, verbatim, the only string a developer can match against their own workspace. A file the URDF never names (an image a .dae loads for itself) is named by joining the mesh’s own directory with that internal reference.
media_type string yes The media type of the stored bytes, as the producer reported it.
size_bytes integer yes How large the stored file is, in bytes.
sha256 string yes The content hash, lowercase hex. Exposed because it is the only way a client can tell “this is the same mesh I already have” across robots — the reason two robots sharing a mesh cost one copy.
created_at string yes When the asset was first stored, as an ISO 8601 timestamp.

asset-list-response

Field Type Required Description
assets object[] yes Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with.
assets[].id string yes The asset’s id in the store.
assets[].robot_id string yes The robot this asset belongs to.
assets[].kind "urdf" | "mesh" | "texture" yes What the file is: the urdf itself, a mesh it references, or a texture a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch.
assets[].name string yes What the robot called it — for a mesh, the package:// URI the URDF references, verbatim, the only string a developer can match against their own workspace. A file the URDF never names (an image a .dae loads for itself) is named by joining the mesh’s own directory with that internal reference.
assets[].media_type string yes The media type of the stored bytes, as the producer reported it.
assets[].size_bytes integer yes How large the stored file is, in bytes.
assets[].sha256 string yes The content hash, lowercase hex. Exposed because it is the only way a client can tell “this is the same mesh I already have” across robots — the reason two robots sharing a mesh cost one copy.
assets[].created_at string yes When the asset was first stored, as an ISO 8601 timestamp.
active_sync object | null yes The sync running right now, or null. It is on this list so a page that reloads and has lost the sync id can still show progress — a freshly loaded page presses no button, it asks this list.
active_sync.sync_id string yes The sync this status describes.
active_sync.robot_id string yes The robot whose assets are being synced.
active_sync.state "running" | "succeeded" | "failed" yes Whether the sync is still running, or ended succeeded or failed. It ends succeeded only when nothing was left behind: a single entry in failed makes the whole sync failed.
active_sync.done integer yes How many files have been transferred so far.
active_sync.total integer yes How many files this sync set out to transfer. It is 0 until the producer has finished working out what there is.
active_sync.failed object[] yes What could not be provided, one entry per reference, each saying why. Required rather than optional: a sync that quietly drops three meshes and reports success moves the failure into somebody’s renderer, where it shows up as a robot with missing limbs and no cause. At most 1000 entries — a producer at its own ceiling reports one entry saying so rather than growing the list.
active_sync.failed[].reference string yes What could not be provided, verbatim — the same string the asset would have been stored under, so a developer can match it against their own workspace by eye. For a failed URDF upload it is robot_description, which is not a mesh URI: a consumer must not assume every entry is one.
active_sync.failed[].kind "unresolvable" | "upload_failed" | "refused" yes Why it failed. unresolvable means the reference names nothing the producer can find or may read, and is permanent — the only kind reconciliation may treat as gone. upload_failed means the bytes exist and the transfer did not succeed, and refused means it was never attempted, either because the robot’s asset store had no room — then details carries the three numbers — or because a producer-side ceiling was hit.
active_sync.failed[].details object | null no The three numbers behind a refused entry the robot’s store had no room for, and absent for every other kind — a forced null on every unresolvable entry buys nothing. A refused entry may also carry no details: the producer’s own ceiling is the other half of that kind, and no store number describes it.
active_sync.failed[].details.store_bytes integer yes The robot’s store, in bytes.
active_sync.failed[].details.used_bytes integer yes Bytes the robot’s assets occupy before this upload.
active_sync.failed[].details.size_bytes integer yes The refused upload, in bytes.
active_sync.reason string | null yes Why the sync ended as it did, when that is not a per-reference fact. null when failed already says everything there is to say.
active_sync.stored integer | null yes How many of the announced files the cloud’s store actually holds. Counted once, after the robot reports the sync done, and null until then — nobody has looked yet. Read it against announced: state is what the robot reported, this is what arrived.
active_sync.announced integer yes How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. 0 when the robot announced nothing, and also 0 until it has answered at all: read it beside stored, which stays null until the terminal frame.
active_sync.started_at string yes When the sync started, as an ISO 8601 timestamp.
active_sync.updated_at string yes When this status last changed, as an ISO 8601 timestamp. A sync that stops moving is visible here rather than only in state.
urdf object yes Whether the stored URDF can actually be rendered, and what it is still missing. Not the same question as whether one was uploaded.
urdf.present boolean yes Whether a URDF has been synced at all. Whether one could be synced is a different question, answered by urdf_available.
urdf.mesh_count integer yes How many distinct meshes the URDF references.
urdf.missing object[] yes The references nothing in the store answers, each with the element that asked for it. A bare count would send a developer hunting through the workspace by hand; the references are what they can act on.
urdf.missing[].uri string yes The reference, verbatim, that no stored asset answers — a package:// URI the workspace does not hold, or an absolute or bare relative path nothing will ever fetch.
urdf.missing[].element "mesh" | "texture" yes Which kind of reference it was: geometry the URDF names as a mesh, or a texture a surface paints with. Without it a client reports a missing texture as a missing mesh, contradicting mesh_count beside it.
urdf_available boolean | null yes What the connected bridge says it could transfer — deliberately separate from what has been transferred. null when no bridge is connected, distinct from false: “no robot is online to ask” and “the robot has no URDF” send a developer to different places. After a publisher is killed rather than shut down this can read true for some seconds, on the underlying DDS liveliness timeout rather than on any check made here.
store object yes How full this robot’s store is.
store.bytes integer yes The robot’s asset store, ROBOT_ASSET_STORE_BYTES.
store.used_bytes integer yes Bytes its assets occupy.
joint_state_slug string | null yes The whole-message sensor_msgs/msg/JointState datapoint that drives the console’s URDF viewer; null when none is chosen or a publish removed it. Set through PUT /api/robots/:id/urdf/joint-state.

asset-sync-request

Field Type Required Description
source "bridge" yes Where the bytes come from. bridge is the only value today: the connected bridge reads them from the robot’s own workspace. Validated rather than ignored, so a caller naming an unknown source is told so instead of silently getting a bridge sync.

asset-sync-response

Field Type Required Description
sync_id string yes The sync that has just started. A sync is long-running, so the answer is something to watch rather than a status that was true at the moment of asking.

asset-sync-status

Field Type Required Description
sync_id string yes The sync this status describes.
robot_id string yes The robot whose assets are being synced.
state "running" | "succeeded" | "failed" yes Whether the sync is still running, or ended succeeded or failed. It ends succeeded only when nothing was left behind: a single entry in failed makes the whole sync failed.
done integer yes How many files have been transferred so far.
total integer yes How many files this sync set out to transfer. It is 0 until the producer has finished working out what there is.
failed object[] yes What could not be provided, one entry per reference, each saying why. Required rather than optional: a sync that quietly drops three meshes and reports success moves the failure into somebody’s renderer, where it shows up as a robot with missing limbs and no cause. At most 1000 entries — a producer at its own ceiling reports one entry saying so rather than growing the list.
failed[].reference string yes What could not be provided, verbatim — the same string the asset would have been stored under, so a developer can match it against their own workspace by eye. For a failed URDF upload it is robot_description, which is not a mesh URI: a consumer must not assume every entry is one.
failed[].kind "unresolvable" | "upload_failed" | "refused" yes Why it failed. unresolvable means the reference names nothing the producer can find or may read, and is permanent — the only kind reconciliation may treat as gone. upload_failed means the bytes exist and the transfer did not succeed, and refused means it was never attempted, either because the robot’s asset store had no room — then details carries the three numbers — or because a producer-side ceiling was hit.
failed[].details object | null no The three numbers behind a refused entry the robot’s store had no room for, and absent for every other kind — a forced null on every unresolvable entry buys nothing. A refused entry may also carry no details: the producer’s own ceiling is the other half of that kind, and no store number describes it.
failed[].details.store_bytes integer yes The robot’s store, in bytes.
failed[].details.used_bytes integer yes Bytes the robot’s assets occupy before this upload.
failed[].details.size_bytes integer yes The refused upload, in bytes.
reason string | null yes Why the sync ended as it did, when that is not a per-reference fact. null when failed already says everything there is to say.
stored integer | null yes How many of the announced files the cloud’s store actually holds. Counted once, after the robot reports the sync done, and null until then — nobody has looked yet. Read it against announced: state is what the robot reported, this is what arrived.
announced integer yes How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. 0 when the robot announced nothing, and also 0 until it has answered at all: read it beside stored, which stays null until the terminal frame.
started_at string yes When the sync started, as an ISO 8601 timestamp.
updated_at string yes When this status last changed, as an ISO 8601 timestamp. A sync that stops moving is visible here rather than only in state.

assets-clear-response

Field Type Required Description
deleted integer yes How many assets — URDF, meshes and textures together — were removed.
bytes_freed integer yes The bytes the robot’s store got back.

audit-list-response

Field Type Required Description
events object[] yes
events[].id string yes
events[].org_id string yes
events[].at string yes
events[].seq integer yes
events[].actor object yes
events[].actor.kind "developer" | "end_user" | "app_user" | "server_key" | "bridge" | "fleetless" yes
events[].actor.id string yes
events[].actor.label string yes
events[].action string yes
events[].target object | null yes
events[].target.kind string yes
events[].target.id string yes
events[].target.label string yes
events[].details record<string, unknown> | null yes
next_cursor integer | null yes

audit-query

Field Type Required Description
before_seq string | integer no
limit string | integer no
action string no
action_prefix string no
actor_id string no
target_kind string no
target_id string no Events whose target is this id; for a robot also the events that name it in details.robot_id (action.invoked, service.called, …), so a robot’s events include what was started on it.
from_ms string | integer no
to_ms string | integer no

auth-me-response

Field Type Required Description
org object yes
org.id string yes The organisation. Every developer route is scoped to the caller’s org already, so a client rarely has to send this anywhere.
org.name string yes The organisation’s display name. Free text, changed through PATCH /api/org.
org.require_two_factor boolean yes Whether every member must have a second factor — a passkey or an authenticator app. A member without one sets it up at their next sign-in, before any session exists; nobody is signed out when it is switched on. It covers the console and the central MCP endpoint; server keys and robot bridges are not people and are not affected. Owners change it through PATCH /api/org.
org.created_at string yes When the organisation was created, as an ISO 8601 timestamp.
user object yes
user.id string yes The Fleetless user in the API, assigned by the cloud and stable for the life of the account.
user.org_id string yes The organisation this person belongs to. Every developer route is already scoped to the caller’s org, so this confirms what a client is looking at, not a filter it applies.
user.email string yes The address the account is identified by, globally unique across every organisation. Immutable after creation: it is what every invitation, reset link and audit line names.
user.display_name string | null yes Optional human name, shown by the console instead of the address where present. Self-service through PATCH /api/auth/me; never used for authentication. null when the person never supplied one.
user.tier "owner" | "developer" yes The console powers this person holds. Required — every Fleetless user has a tier; it was optional only while the org also held people with no console powers to grade, and that pool is gone.
user.two_factor object yes The person’s second factors, as the team list shows them: none, passkeys, an authenticator, or both. No credential travels here. An owner resets them through DELETE /api/org/users/:id/two-factor.
user.two_factor.passkeys integer yes How many passkeys the person has registered.
user.two_factor.authenticator boolean yes Whether the person has a confirmed authenticator app.
user.created_at string yes When the account was created, as an ISO 8601 timestamp.

authorization-server-metadata

Field Type Required Description
issuer string yes The issuer identifier of this authorization server, per RFC 8414 §2. It is what a client checks a token’s iss against.
authorization_endpoint string yes Where a client sends the user to authorize.
token_endpoint string yes The URL where a client exchanges an authorization code, or a refresh token, for tokens.
registration_endpoint string no The URL where a client may register itself, per RFC 7591. Absent when the app does not accept dynamic clients.
response_types_supported "code"[] yes The response types this server offers: code only, the implicit grant being gone with OAuth 2.1.
grant_types_supported "authorization_code" | "refresh_token"[] yes The grants this server offers: authorization_code and refresh_token. OAuth 2.1 removes the implicit and password grants, so neither appears here.
code_challenge_methods_supported "S256"[] yes The PKCE challenge methods accepted: S256 only. plain is not offered — a challenge equal to its verifier defends against nothing, and offering it would make a downgrade negotiable.
token_endpoint_auth_methods_supported "none"[] yes How a client authenticates at the token endpoint: none, the public-client method, with PKCE protecting the exchange.
scopes_supported string[] no The scopes this server knows about, where it publishes a list.

billing-cancel-request

Field Type Required Description
reason string no An optional free-text reason. Shown to nobody but Fleetless.

billing-change-request

Field Type Required Description
plan "plus" | "pro" no The target plan. Omitted leaves the plan as it is.
cycle "monthly" | "yearly" no The target cycle. Omitted leaves the cycle as it is.
addons object no Absolute add-on counts to end up with, not a delta. Omitted leaves add-ons as they are.
addons.seats integer no Extra developer seats, one each.
addons.robots integer no Extra robots, one each.
addons.apps integer no Extra apps, one each.
addons.app_user_packs integer no Packs of five extra app users.
addons.live_video_packs integer no Packs of 250 extra hours of app-user live video per month.

billing-change-response

Field Type Required Description
billing object yes The billing view after the change.
billing.available boolean yes Whether this cloud takes payments at all — false when no Mollie key is configured.
billing.account object | null yes null before the org has ever checked out.
billing.account.payer object yes Who is paying, and the invoice address and email.
billing.account.payer.kind "company" yes Billed as a company.
billing.account.payer.company_name string yes The company’s legal name, printed on the invoice.
billing.account.payer.vat_id string | null yes The company’s VAT ID, or null for none. Required, and must check out through VIES, for a company outside Germany.
billing.account.payer.address object yes The billing address.
billing.account.payer.address.line1 string yes Street and number, or the first address line.
billing.account.payer.address.line2 string | null yes A second address line, or null when there is none.
billing.account.payer.address.postal_code string yes Postal or ZIP code.
billing.account.payer.address.city string yes City or town.
billing.account.payer.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
billing.account.payer.invoice_email string yes Where invoices and billing mail are sent.
billing.account.payer.kind "person" yes Billed as a person.
billing.account.payer.full_name string yes The person’s full name, printed on the invoice.
billing.account.payer.address object yes The billing address.
billing.account.payer.address.line1 string yes Street and number, or the first address line.
billing.account.payer.address.line2 string | null yes A second address line, or null when there is none.
billing.account.payer.address.postal_code string yes Postal or ZIP code.
billing.account.payer.address.city string yes City or town.
billing.account.payer.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
billing.account.payer.invoice_email string yes Where invoices and billing mail are sent.
billing.account.vat_id_status "valid" | "unverified" | "invalid" | null yes The payer’s VAT ID check, or null when no VAT ID was given.
billing.account.vat object yes The VAT treatment and rate this account charges at (vatFor).
billing.account.vat.treatment "de_standard" | "reverse_charge" | "outside_eu" yes How the account is taxed.
billing.account.vat.rate_percent integer yes The VAT rate, in whole percent.
billing.account.currency "eur" | "usd" yes The org’s billing currency, fixed at the first payment.
billing.account.cycle "monthly" | "yearly" yes The current billing cycle.
billing.account.status "pending" | "active" | "past_due" | "canceled" yes The account’s own status.
billing.account.period_starts_at string | null yes The current period’s start. null before the first payment.
billing.account.period_ends_at string | null yes The current period’s end. null before the first payment.
billing.account.next_charge object | null yes null while a cancel is pending or nothing else renews.
billing.account.next_charge.at string yes When the next charge is due.
billing.account.next_charge.net_cents integer yes The next charge, excluding VAT, in integer cents.
billing.account.next_charge.vat_cents integer yes VAT on the next charge, in integer cents.
billing.account.next_charge.gross_cents integer yes The next charge including VAT, in integer cents.
billing.account.scheduled object yes What takes effect at the period’s end.
billing.account.scheduled.cycle "monthly" | "yearly" | null yes A cycle change queued for the period’s end, or null.
billing.account.scheduled.addons object | null yes Add-on counts queued for the period’s end, or null.
billing.account.scheduled.addons.seats integer yes Extra developer seats, one each.
billing.account.scheduled.addons.robots integer yes Extra robots, one each.
billing.account.scheduled.addons.apps integer yes Extra apps, one each.
billing.account.scheduled.addons.app_user_packs integer yes Packs of five extra app users.
billing.account.scheduled.addons.live_video_packs integer yes Packs of 250 extra hours of app-user live video per month.
billing.account.dunning object | null yes null while nothing is overdue. During a VAT-ID hold the charge is due but not yet invoiced.
billing.account.dunning.invoice_id string yes The open invoice dunning is chasing — or, while the payer’s VAT ID is unsettled, the due charge that becomes one: it has no number yet and is not in invoices. POST /api/billing/invoices/:id/pay takes either.
billing.account.dunning.gross_cents integer yes The amount owed, in integer cents — after a partial chargeback only the charged-back part.
billing.account.dunning.due_at string yes The charge’s due date; retries and the lock count from here.
billing.account.dunning.next_retry_at string | null yes The next automatic retry, or null once retries are exhausted. Always null after a chargeback: a charged-back payment is never charged again automatically.
billing.account.dunning.lock_at string yes When the org is locked if still unpaid (BILLING_LOCK_DAY).
billing.account.dunning.failure string | null yes Mollie’s own reason, e.g. ‘card_expired’; ‘vat_id_invalid’ while a renewal is held because VIES does not confirm the VAT ID; ‘charged_back’ after a chargeback or a lost PayPal dispute.
billing.payment_method object | null yes The payment method on file, or null.
billing.payment_method_options object[] yes What a payment-method change may offer right now, so a client offers exactly what the cloud accepts. applepay is absent when it needs an open invoice and none is open; [] when the org has no billing account.
billing.payment_method_options[].method "card" | "paypal" | "applepay" yes A method POST /api/billing/payment-method accepts now.
billing.payment_method_options[].pays_invoice boolean yes Whether changing to it also pays the open invoice — true only for applepay when a zero-amount Apple Pay payment is not available.
billing.invoices object[] yes Newest first, at most 24.
billing.invoices[].id string yes The invoice’s id.
billing.invoices[].number string yes The invoice number: FL-<year>-<seq>, seq zero-padded to four digits, gapless per calendar year in Europe/Berlin.
billing.invoices[].issued_at string yes When the invoice was issued.
billing.invoices[].status "open" | "paid" | "uncollectible" yes This invoice’s own status.
billing.invoices[].currency "eur" | "usd" yes The org’s billing currency.
billing.invoices[].net_cents integer yes The charge, excluding VAT, in integer cents.
billing.invoices[].vat_rate_percent integer yes The VAT rate applied, in whole percent.
billing.invoices[].vat_cents integer yes VAT, in integer cents (vatCents).
billing.invoices[].gross_cents integer yes Net plus VAT, in integer cents.
charged object | null yes The invoice charged now, or null when the change was only scheduled for the period’s end.
charged.id string yes The invoice’s id.
charged.number string yes The invoice number: FL-<year>-<seq>, seq zero-padded to four digits, gapless per calendar year in Europe/Berlin.
charged.issued_at string yes When the invoice was issued.
charged.status "open" | "paid" | "uncollectible" yes This invoice’s own status.
charged.currency "eur" | "usd" yes The org’s billing currency.
charged.net_cents integer yes The charge, excluding VAT, in integer cents.
charged.vat_rate_percent integer yes The VAT rate applied, in whole percent.
charged.vat_cents integer yes VAT, in integer cents (vatCents).
charged.gross_cents integer yes Net plus VAT, in integer cents.

billing-details-update

Field Type Required Description
invoice_email string no Replaces the invoice email. Omitted leaves it as it is.
vat_id string | null no Replaces the VAT ID; null clears it. Omitted leaves it as it is. A new ID is re-checked through VIES.

billing-view

Field Type Required Description
available boolean yes Whether this cloud takes payments at all — false when no Mollie key is configured.
account object | null yes null before the org has ever checked out.
account.payer object yes Who is paying, and the invoice address and email.
account.payer.kind "company" yes Billed as a company.
account.payer.company_name string yes The company’s legal name, printed on the invoice.
account.payer.vat_id string | null yes The company’s VAT ID, or null for none. Required, and must check out through VIES, for a company outside Germany.
account.payer.address object yes The billing address.
account.payer.address.line1 string yes Street and number, or the first address line.
account.payer.address.line2 string | null yes A second address line, or null when there is none.
account.payer.address.postal_code string yes Postal or ZIP code.
account.payer.address.city string yes City or town.
account.payer.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
account.payer.invoice_email string yes Where invoices and billing mail are sent.
account.payer.kind "person" yes Billed as a person.
account.payer.full_name string yes The person’s full name, printed on the invoice.
account.payer.address object yes The billing address.
account.payer.address.line1 string yes Street and number, or the first address line.
account.payer.address.line2 string | null yes A second address line, or null when there is none.
account.payer.address.postal_code string yes Postal or ZIP code.
account.payer.address.city string yes City or town.
account.payer.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
account.payer.invoice_email string yes Where invoices and billing mail are sent.
account.vat_id_status "valid" | "unverified" | "invalid" | null yes The payer’s VAT ID check, or null when no VAT ID was given.
account.vat object yes The VAT treatment and rate this account charges at (vatFor).
account.vat.treatment "de_standard" | "reverse_charge" | "outside_eu" yes How the account is taxed.
account.vat.rate_percent integer yes The VAT rate, in whole percent.
account.currency "eur" | "usd" yes The org’s billing currency, fixed at the first payment.
account.cycle "monthly" | "yearly" yes The current billing cycle.
account.status "pending" | "active" | "past_due" | "canceled" yes The account’s own status.
account.period_starts_at string | null yes The current period’s start. null before the first payment.
account.period_ends_at string | null yes The current period’s end. null before the first payment.
account.next_charge object | null yes null while a cancel is pending or nothing else renews.
account.next_charge.at string yes When the next charge is due.
account.next_charge.net_cents integer yes The next charge, excluding VAT, in integer cents.
account.next_charge.vat_cents integer yes VAT on the next charge, in integer cents.
account.next_charge.gross_cents integer yes The next charge including VAT, in integer cents.
account.scheduled object yes What takes effect at the period’s end.
account.scheduled.cycle "monthly" | "yearly" | null yes A cycle change queued for the period’s end, or null.
account.scheduled.addons object | null yes Add-on counts queued for the period’s end, or null.
account.scheduled.addons.seats integer yes Extra developer seats, one each.
account.scheduled.addons.robots integer yes Extra robots, one each.
account.scheduled.addons.apps integer yes Extra apps, one each.
account.scheduled.addons.app_user_packs integer yes Packs of five extra app users.
account.scheduled.addons.live_video_packs integer yes Packs of 250 extra hours of app-user live video per month.
account.dunning object | null yes null while nothing is overdue. During a VAT-ID hold the charge is due but not yet invoiced.
account.dunning.invoice_id string yes The open invoice dunning is chasing — or, while the payer’s VAT ID is unsettled, the due charge that becomes one: it has no number yet and is not in invoices. POST /api/billing/invoices/:id/pay takes either.
account.dunning.gross_cents integer yes The amount owed, in integer cents — after a partial chargeback only the charged-back part.
account.dunning.due_at string yes The charge’s due date; retries and the lock count from here.
account.dunning.next_retry_at string | null yes The next automatic retry, or null once retries are exhausted. Always null after a chargeback: a charged-back payment is never charged again automatically.
account.dunning.lock_at string yes When the org is locked if still unpaid (BILLING_LOCK_DAY).
account.dunning.failure string | null yes Mollie’s own reason, e.g. ‘card_expired’; ‘vat_id_invalid’ while a renewal is held because VIES does not confirm the VAT ID; ‘charged_back’ after a chargeback or a lost PayPal dispute.
payment_method object | null yes The payment method on file, or null.
payment_method_options object[] yes What a payment-method change may offer right now, so a client offers exactly what the cloud accepts. applepay is absent when it needs an open invoice and none is open; [] when the org has no billing account.
payment_method_options[].method "card" | "paypal" | "applepay" yes A method POST /api/billing/payment-method accepts now.
payment_method_options[].pays_invoice boolean yes Whether changing to it also pays the open invoice — true only for applepay when a zero-amount Apple Pay payment is not available.
invoices object[] yes Newest first, at most 24.
invoices[].id string yes The invoice’s id.
invoices[].number string yes The invoice number: FL-<year>-<seq>, seq zero-padded to four digits, gapless per calendar year in Europe/Berlin.
invoices[].issued_at string yes When the invoice was issued.
invoices[].status "open" | "paid" | "uncollectible" yes This invoice’s own status.
invoices[].currency "eur" | "usd" yes The org’s billing currency.
invoices[].net_cents integer yes The charge, excluding VAT, in integer cents.
invoices[].vat_rate_percent integer yes The VAT rate applied, in whole percent.
invoices[].vat_cents integer yes VAT, in integer cents (vatCents).
invoices[].gross_cents integer yes Net plus VAT, in integer cents.

busy-details

Field Type Required Description
running object yes
running.id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
running.robot_id string yes The robot this job is running on.
running.slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
running.state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
running.origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
running.started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
running.updated_at string yes When this job last changed, as an ISO 8601 timestamp.
running.seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
running.result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
running.error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
running.error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
running.error.message string yes A human-readable sentence saying what went wrong.
running.error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.

camera-descriptor

Field Type Required Description
slug string yes The name a client addresses this camera by.
width integer yes Frame width in pixels, as the published configuration declares it.
height integer yes Frame height in pixels, as the published configuration declares it.
fps integer yes How many frames per second the camera is configured to publish while somebody is watching live.
snapshot_interval_seconds integer yes How often a still frame is captured for the cheap snapshot reads, in seconds, between 1 and 3600. Independent of fps, which is about live video.

camera-list-response

Field Type Required Description
cameras object[] yes Every camera the published configuration exposes on this robot and the caller’s role grants. A developer sees all of them; an end user sees what their role allows.
cameras[].slug string yes The name a client addresses this camera by.
cameras[].width integer yes Frame width in pixels, as the published configuration declares it.
cameras[].height integer yes Frame height in pixels, as the published configuration declares it.
cameras[].fps integer yes How many frames per second the camera is configured to publish while somebody is watching live.
cameras[].snapshot_interval_seconds integer yes How often a still frame is captured for the cheap snapshot reads, in seconds, between 1 and 3600. Independent of fps, which is about live video.

cancel-rejected-details

Field Type Required Description
goals object[] yes
goals[].job_id string yes
goals[].goal_id string yes
goals[].return_code integer | null yes

cancel-request

Field Type Required Description
job_id string | null no The one job to stop. Absent or null cancels whatever is currently running on the slug, which is what every caller written before this field existed means. Unknown keys are refused rather than stripped, so a misspelling cannot silently become the slug-wide cancel.

checkout-request

Field Type Required Description
plan "plus" | "pro" yes The plan to buy.
cycle "monthly" | "yearly" yes Monthly, or yearly at 15 % off.
addons object no Add-on counts to buy alongside the plan. Pro only; named on plus, or without the feature, 400 validation_error.
addons.seats integer no Extra developer seats, one each.
addons.robots integer no Extra robots, one each.
addons.apps integer no Extra apps, one each.
addons.app_user_packs integer no Packs of five extra app users.
addons.live_video_packs integer no Packs of 250 extra hours of app-user live video per month.
billing object yes Who is paying, and where the invoice goes.
billing.kind "company" yes Billed as a company.
billing.company_name string yes The company’s legal name, printed on the invoice.
billing.vat_id string | null yes The company’s VAT ID, or null for none. Required, and must check out through VIES, for a company outside Germany.
billing.address object yes The billing address.
billing.address.line1 string yes Street and number, or the first address line.
billing.address.line2 string | null yes A second address line, or null when there is none.
billing.address.postal_code string yes Postal or ZIP code.
billing.address.city string yes City or town.
billing.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
billing.invoice_email string yes Where invoices and billing mail are sent.
billing.kind "person" yes Billed as a person.
billing.full_name string yes The person’s full name, printed on the invoice.
billing.address object yes The billing address.
billing.address.line1 string yes Street and number, or the first address line.
billing.address.line2 string | null yes A second address line, or null when there is none.
billing.address.postal_code string yes Postal or ZIP code.
billing.address.city string yes City or town.
billing.address.country string yes The billing country. Decides VAT (vatFor) and currency (currencyForCountry).
billing.invoice_email string yes Where invoices and billing mail are sent.
accept_terms true yes Confirms Fleetless’s terms of service. Always required.
accept_withdrawal true no Confirms the plan starts at once and the 14-day right of withdrawal ends with it. Required for a person; a company sending it is 400 validation_error — it has no withdrawal right to confirm.

checkout-response

Field Type Required Description
checkout_id string yes Identifies this checkout: polled by GET /api/billing/checkout/:id and carried on the return URL.
checkout_url string yes Mollie’s hosted checkout page. The caller’s browser is sent here.

checkout-status

Field Type Required Description
checkout_id string yes The checkout this status is for.
status "pending" | "paid" | "failed" | "canceled" | "expired" yes Mollie’s payment status, as reconcilePayment last read it.
purpose "upgrade" | "payment_method" | "invoice" yes What this checkout paid for: a plan upgrade, a payment-method change, or an open invoice.
plan "basic" | "plus" | "pro" | "enterprise" yes The org’s plan after applying — unchanged unless purpose is upgrade and status is paid.

client-accept-invitation-request

Field Type Required Description
token string yes The opaque token from the invitation link, valid seven days. Unknown, expired, revoked and already-accepted all answer 410 token_spent.
password string no The password the new account will use, at least 12 characters. Required while the app’s password method is on, refused while it is off — both as 400 validation_error naming password. An email-code-only app accepts invitations without one.
display_name string | null no An optional name, overriding whatever the invitation pre-filled.

client-identity

Field Type Required Description
kind "developer" | "app_user" | "server_key" yes Which of the three kinds of caller this is: a developer working through the console, an app_user holding a token from a client login, or a server_key used by server-side code. Stated outright, not inferred from which id is set.
developer_id string | null yes The Fleetless user behind this session, or null when kind is not developer.
app_user_id string | null yes The app user behind this session, or null when kind is not app_user. An app user belongs to exactly one app and is unrelated to any Fleetless user with the same address.
server_key_id string | null yes The server key this session was authenticated with, or null when kind is not server_key.
app_id string | null yes The app this session belongs to, and null for a developer — a developer is organisation-scoped and owns the configuration of every robot in the organisation rather than reaching one through an app.
role_id string | null yes The role that decides what this caller may reach, and null for a developer. Roles are the only visibility filter: what a role does not grant does not exist for that user.
email string | null yes The address of the Fleetless user or app user behind this session, and null for a server key, which is not a person.
two_factor_enabled boolean | null yes Whether the app user has a confirmed authenticator, so the app’s account settings can offer to turn it on or off. null unless kind is app_user.

client-login-code-request

Field Type Required Description
app_identifier string yes The app to sign in to. An identifier no app carries is 404 not_found; the address is never the subject of a refusal.
email string yes The address to mail the code to, trimmed and compared case-insensitively. 202 whether or not it names an account of this app.

client-login-code-verify-request

Field Type Required Description
app_identifier string yes The app the code was requested for.
email string yes The address the code was mailed to, as typed when it was requested; trimmed and compared case-insensitively.
code string yes The six digits from the mail, exactly — leading zeros included, no spaces.

client-login-request

Field Type Required Description
app_identifier string yes The app being logged in to: its globally unique, lowercase, underscore-separated identifier, chosen by the developer at creation. There is no organisation context at login, so this is what decides which app the credentials are checked for.
email string yes The app user’s address. Addresses are unique per app, not across Fleetless: the same address may be an unrelated account in another app of the same organisation, so this pair is what identifies a person here.
password string yes The app user’s password. A wrong pair is refused without saying which half was wrong — and a blocked or not-yet-verified account is refused identically, so a failed login is not an account-enumeration oracle in any of its three forms.

client-logout-request

Field Type Required Description
refresh_token string yes Any refresh token of the session to end. The whole token family is revoked server-side, so a token stolen before this call stops working too — clearing a client-side store is a gesture, not a revocation. The answer is 204: a token the server does not recognise gets it too, since that is the end state being asked for.

client-mcp-interaction

Field Type Required Description
id string yes The interaction, as it arrived in the app’s mcp_login_url. Not a credential: it names a pending request the server already holds, and approving it needs the app user’s own access token.
app_id string yes The app this authorization is for. The approving token’s app_id must match it — an interaction of one app cannot be approved with a session from another.
client_name string | null yes What the MCP client calls itself, or null if it named nothing. Unverified — see client_name_verified.
client_name_verified false yes Always false. The client registered itself without authentication and named itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a true branch would be dead code that looked like a safeguard.
scopes string[] yes The scopes the client asked for, to show the person before they approve.
already_granted boolean yes Whether this user has already approved this client. It is a record of what they answered last time, and this route makes no second use of it: an app that skips its own consent screen when this is true is the only thing deciding that, and approve succeeds identically for a user who holds no grant at all. The standing grant is read elsewhere, on every request to the app’s MCP endpoint. Withdrawing it is DELETE /api/client/mcp/grants/:clientId for the person themselves and DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId for the developer. A withdrawal makes this false again at the next authorization and stops the client at its very next MCP call, unexpired access token and all — up to fifteen minutes of it — because the endpoint keys that check on the client_id the token carries.
expires_at string yes When the interaction stops being approvable. Ten minutes from the authorize step; afterwards both approve and deny answer interaction_expired.

client-mcp-interaction-decision-response

Field Type Required Description
redirect_to string yes Send the browser here. It is the MCP client’s own callback, carrying either the authorization code or error=access_denied — a denial redirects as well, so the client learns the outcome from the place it is waiting.

client-oidc-callback-query

Field Type Required Description
state string yes The opaque state Fleetless sent to the provider, which resolves the pending interaction — and with it the app’s redirect_uri. Not the app’s own state from start: that one is stored on the interaction and put back on the redirect to the app. A callback whose state resolves to nothing has no confirmed target to answer, and is the one case Fleetless renders a page for.
code string no The provider’s authorization code, present when the sign-in succeeded. Exchanged server-side by the cloud, so it never reaches the app — the app gets its own one-time code, bound to the PKCE challenge it sent at start.
error string no The provider’s own refusal, present instead of code when the person declined or the provider would not issue one. It is carried back to the app as a clientOidcErrorCode, not passed through: the provider’s vocabulary is its own, and an app branching on it would be branching on a string nobody here controls.
error_description string no The provider’s human-readable note about error, when it sends one. Logged, never rendered to an app user and never put on the redirect — it is text from a system Fleetless does not run.

client-oidc-exchange-request

Field Type Required Description
code string yes The one-time code from the callback redirect. Valid 60 seconds, single-use, and bound to the PKCE challenge the start step carried.
code_verifier string yes The verifier for the challenge sent at start. RFC 7636 §4.1’s alphabet and length.

client-oidc-start-query

Field Type Required Description
app_identifier string yes The app being signed in to.
redirect_uri string yes Where to send the browser when the flow finishes, with code and state or with error and state. Its origin must be one of the app’s allowed_origins; a failure here is refused flat, with no redirect, because until the target is confirmed there is nowhere trusted to bounce a browser to.
state string yes Returned unchanged on the callback, and on the error redirect too, so the app can bind either answer to the request it started. At least eight characters: this is what ties the callback to the browser that began the flow, and a guessable value defends nothing.
code_challenge string yes The app’s own PKCE challenge (RFC 7636, S256 base64url). The verifier is presented at oidc/exchange, so the one-time code is worth nothing to whoever intercepts the redirect. plain is not accepted: a challenge equal to its verifier defends against nothing.

client-password-reset-confirm-request

Field Type Required Description
token string yes The opaque token from the reset link, valid one hour. Single-use; unknown, expired and spent all answer 410 token_spent.
new_password string yes The replacement password. Accepting it revokes every refresh family the account holds — the answer carries a fresh pair, so the person is signed in on the device that completed the reset and nowhere else.

client-password-reset-request

Field Type Required Description
app_identifier string yes The app the address belongs to.
email string yes The address to mail a reset link to. The answer is 202 for a known address and an unknown one alike, in status, body and timing. An app identifier no app carries is 404 not_found; the address is never the subject of a refusal.

client-provider-list-query

The one parameter of the public provider listing.

Field Type Required Description
app_identifier string yes The app whose enabled providers to list.

client-provider-list-response

Field Type Required Description
providers object[] yes The app’s enabled providers, slug and display name only. An app with none answers an empty array, which is the state of an app that offers password or code sign-in alone.
providers[].slug string yes The handle to put in the start URL: GET /api/client/oidc/<slug>/start.
providers[].name string yes What to write on the button, as the developer configured it.
sign_in_methods object yes Which of password and emailed code the app accepts, so its sign-in page draws the right fields without guessing. The same value the developer set; public, like the provider buttons.
sign_in_methods.password boolean yes Whether app users may sign in with a password. Off refuses POST /api/client/login with method_not_allowed, and registration and invitations then take no password.
sign_in_methods.email_code boolean yes Whether app users may sign in with a six-digit code mailed to them, valid ten minutes. A code needs no URL, so it works in local development and in an app with no web UI.

client-refresh-request

Field Type Required Description
refresh_token string yes The refresh token from the last login or refresh. Refresh tokens rotate on every use, so the value sent here is spent — keep the one that comes back, and presenting a spent one is treated as theft and ends the whole family.

client-register-request

Field Type Required Description
app_identifier string yes The app to register with. An identifier no app carries is 404 not_found — an identifier is public, so naming it is no disclosure, and collapsing it into registration_closed sent a developer who mistyped their own identifier hunting a configuration bug that was not there. The address is never the subject of a refusal.
email string yes The address to register. Unique per app, case-insensitively. An address this app already knows still answers 202, without a mail — the answer may not say whether an account exists.
password string no The password for the new account, at least 12 characters. Required while the app’s password method is on, refused while it is off — both as 400 validation_error naming password. An email-code-only app registers people without one.
display_name string | null no An optional human name for the account. The developer’s own UI decides whether to ask for it.

client-resend-verification-request

Field Type Required Description
app_identifier string yes The app the address belongs to.
email string yes The address to re-send to. The answer is 202 whether or not it names an account, and whether or not that account is already verified.

client-robot-list-item

Field Type Required Description
id string yes The robot, and what every robot-scoped route takes as its :id.
name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
created_at string yes When the robot was created, as an ISO 8601 timestamp.
bridge_state object yes The built-in bridge_state datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is.
bridge_state.online boolean yes
bridge_state.latency_ms number | null yes
bridge_state.low_bandwidth boolean yes Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported.
published_version integer | null yes The published configuration version, or null when nothing has been published yet. A robot with nothing published is still listed — “not configured yet” is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list.

client-robot-list-response

Field Type Required Description
robots object[] yes Every robot the caller reaches, in name order with the id as the tiebreak. An app user reaches the robots their app attaches on which their role grants at least one slug or capability; a server key reaches every robot its app attaches; a developer reaches every robot of the organisation.
robots[].id string yes The robot, and what every robot-scoped route takes as its :id.
robots[].name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
robots[].created_at string yes When the robot was created, as an ISO 8601 timestamp.
robots[].bridge_state object yes The built-in bridge_state datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is.
robots[].bridge_state.online boolean yes
robots[].bridge_state.latency_ms number | null yes
robots[].bridge_state.low_bandwidth boolean yes Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported.
robots[].published_version integer | null yes The published configuration version, or null when nothing has been published yet. A robot with nothing published is still listed — “not configured yet” is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list.

client-sign-in-result

Field Type Required Description
access_token string yes The token to send as Authorization: Bearer <token> on every call. Short-lived: read expires_in rather than assuming a lifetime.
refresh_token string yes The token that buys the next access token. It rotates on every use, so a value presented twice is detectable theft and ends the whole family.
expires_in integer yes How long the access token stays valid, in seconds from now. Not a timestamp, and not milliseconds.
status "two_factor_required" | "two_factor_setup_required" yes two_factor_required: ask for the authenticator code. two_factor_setup_required: the app requires two-factor and the person has none yet, so set one up before any session exists.
challenge string yes The handle the next step spends. Valid five minutes; afterwards it answers 410 token_spent and the sign-in starts over.

client-two-factor-disable-request

Field Type Required Description
code string yes A code the authenticator shows now.

client-two-factor-setup-confirm-request

Field Type Required Description
challenge string no The same challenge as at setup, during sign-in; absent with a bearer.
code string yes A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it.

client-two-factor-setup-confirm-response

Field Type Required Description
recovery_codes string[] yes The ten single-use recovery codes, lowercase, shown once. Any earlier set is void.
session object yes The session the sign-in was waiting for, or a fresh one for the account settings.
session.access_token string yes The token to send as Authorization: Bearer <token> on every call. Short-lived: read expires_in rather than assuming a lifetime.
session.refresh_token string yes The token that buys the next access token. It rotates on every use, so a value presented twice is detectable theft and ends the whole family.
session.expires_in integer yes How long the access token stays valid, in seconds from now. Not a timestamp, and not milliseconds.

client-two-factor-setup-request

Field Type Required Description
challenge string no The two_factor_setup_required challenge, during sign-in. Absent when the call carries the app user’s bearer instead.

client-two-factor-verify-request

Field Type Required Description
challenge string yes The challenge the sign-in step answered.
code string no The six-digit code the authenticator shows now. A code already accepted once is refused, so a replay of a seen code does not sign anybody in.
recovery_code string no One of the ten recovery codes, xxxxx-xxxxx, in either case. Spent by its use.

client-verify-email-request

Field Type Required Description
token string yes The opaque token from the verification link, valid 24 hours. Unknown, expired and already-spent all answer 410 token_spent — telling them apart would say whether a token ever existed.

config-draft-response

Field Type Required Description
doc object | null yes
doc.fleetless 1 yes The format version, and the first line of the file. It decides how everything below is read, so a file that omits it — or names a version this cloud does not know — is refused rather than half understood.
doc.messages record<string, unknown> no Reusable message bodies, keyed by name, inserted elsewhere by writing ${name} directly after message:. A shared body may hold placeholders and whoever inserts it declares the parameters, so two publishers can send the same message under different bounds. A shared message may not insert another, so a ${name} inside a body is always a parameter and never a second message.
doc.datapoints record<string, object> no Values the robot publishes, each one field of one topic or a whole topic, and never several topics. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.datapoints.*.topic string yes The ROS topic this datapoint reads, as an absolute graph name. One datapoint reads one topic: a value assembled from two topics is not expressible here.
doc.datapoints.*.type string yes The message type carried by topic, spelled the way ROS 2 spells it, with the msg segment in the middle — sensor_msgs/msg/BatteryState, never sensor_msgs/BatteryState. It is declared here rather than discovered, so a configuration can be written for a robot that has never been connected; the cloud checks it against the robot’s own message definitions only once one is there.
doc.datapoints.*.field string no A dotted path into the message naming the single value this datapoint carries, each segment indexing at most one array level — ranges[0], never ranges[0][1], because ROS 2 has no nested arrays. Without it the datapoint is the whole message, and numeric, chart and alerts are then refused.
doc.datapoints.*.rate_throttle_hz number no A ceiling on how often this datapoint is sent, in hertz. Omitted or 0 means no throttling. It is a ceiling, not a clock: a slow topic stays slow, a value is never repeated to manufacture a rate, and within a window the newest value wins. The bridge enforces it, so the robot’s bandwidth is genuinely saved.
doc.datapoints.*.low_bandwidth "keep" no keep exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.
doc.datapoints.*.description string no Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into robot_describe, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with description: null and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.
doc.datapoints.*.numeric object no Arithmetic and formatting for a numeric value. scale and offset are applied on the robot, before sending, which is why REST, realtime and history all carry identical numbers. unit and decimals change nothing the robot does, so a publish that touches only those pushes no configuration.
doc.datapoints.*.numeric.scale number no A factor the robot multiplies the raw value by before sending it (value * scale + offset). The arithmetic happens once, at the source, so REST, realtime and history can never disagree about a number.
doc.datapoints.*.numeric.offset number no A constant the robot adds after scale (value * scale + offset), for a value whose zero sits in the wrong place. Like scale it is applied before sending, so history stores the converted value and a later correction cannot reach what is already stored.
doc.datapoints.*.numeric.unit string no The unit of the value after scale and offset, not the robot’s own. It is shown beside the value and carried by robot_describe as its own field, so a model does not have to guess whether 15 means percent, volts or minutes.
doc.datapoints.*.numeric.decimals integer no How many fraction digits the console shows the value with — value tile, chart axis and tooltip, and the datapoint detail page — and the number robot_describe reports as its own field, so a model formats the value the way the console does. Presentation only: the stored value keeps the precision it arrived with, and absent means the console’s own default rather than zero.
doc.datapoints.*.retention object no What outlives the moment: whether this value is written to the time series, how often, and how many points the robot buffers while the bridge is away. Absent means no history at all — the value is live only.
doc.datapoints.*.retention.enabled boolean no Whether values are written to the time series and become queryable. Off by default: without it the value is live only, and nobody who was not watching will ever see it.
doc.datapoints.*.retention.interval_seconds integer no How often a value is written to history, in seconds; absent means 300. Not how often it is sent — that is rate_throttle_hz. Stored points are billed, so this is the direct lever on what a robot costs, and a bumper that is true for 200 ms does not appear unless a write falls inside it.
doc.datapoints.*.retention.max_buffer_values integer no How many values the robot holds while the bridge is disconnected, to be pushed once it reconnects. The catch-up runs behind live telemetry and job results at a limited rate, so closing a gap never delays the present; without it the series simply has a gap, which is an honest answer.
doc.datapoints.*.chart object no How the console draws this value over time: axis bounds, whether the line interpolates or steps, and the window a chart opens on. Display only — it changes no stored value, no alert and nothing the robot does, so a publish that touches only it pushes no configuration.
doc.datapoints.*.chart.y_min number no A fixed floor for the chart’s y axis; omitted, the axis scales to the data. 0 is a real floor and is read as 0, never as unset.
doc.datapoints.*.chart.y_max number no A fixed ceiling for the chart’s y axis; omitted, the axis scales to the data. It may not sit below y_min: a reversed pair is refused here because nothing downstream catches it, and the chart would render empty.
doc.datapoints.*.chart.style "line" | "step" no How the drawing joins two samples, which is not a matter of taste. line claims the value moved evenly between them, roughly true of a temperature or a charge; step holds and then jumps, the only honest drawing for a mode, a switch or a counter, where a straight line would show values that never existed.
doc.datapoints.*.chart.default_window_minutes integer no How far back the chart reaches when it is first opened, in minutes; absent means 60. Only the starting zoom: a viewer may look further, and nothing about what is stored follows from it.
doc.datapoints.*.alerts record<string, object> no Alerts watching this value, keyed by slug; each moves between ok and firing and writes an org event on every transition. No mail is sent. The key is the identity, so renaming an alert is a delete plus a create: its runtime state is lost, and an alert that is still true fires again.
doc.datapoints.*.alerts.*.condition object yes When this alert fires and when it is ok again. It carries no discriminator: upper threshold, lower threshold or equality all follow from the two values in it. Editing it resets the alert to ok on the next publish, while a publish that leaves it untouched keeps the running state.
doc.datapoints.*.alerts.*.condition.fire_at number | string | boolean yes The value at which the alert starts firing. Alone it is an equality: it fires while the value equals fire_at and is ok again as soon as it differs, which is what makes a boolean or a string condition meaningful. Adding resolve_at turns it into a threshold instead.
doc.datapoints.*.alerts.*.condition.resolve_at number no The value at which a firing alert becomes ok again — allowed only when fire_at is a number, and it must differ from it. That gap is the hysteresis, and it makes the condition a threshold whose direction follows from which of the two values is higher. Without a gap a value sitting on the line flips on every sample.
doc.datapoints.*.alerts.*.severity "warning" | "error" no How bad it is when this alert fires; absent means warning. It changes no behaviour — nothing is escalated, retried or delivered differently — it travels with the org event and colours the alert wherever it is shown.
doc.datapoints.*.alerts.*.name string no A human-readable label shown wherever this alert appears, in place of its bare key. It is not the alert’s identity — the key is — so the label can be reworded freely, while changing the key deletes one alert and creates another.
doc.datapoints.*.alerts.*.enabled boolean no Whether this alert is evaluated at all. Absent means on, the opposite of retention.enabled: an alert that is written down watches unless it is explicitly switched off, which is how one is silenced without losing the key that identifies it.
doc.actions record<string, object> no Things the robot does on request that take time, each reported as a job with progress. At most one job runs per action slug: a second call is refused busy, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.actions.*.ros_name string yes The action server on the robot, as an absolute graph name — this is what the bridge sends the goal to. Clients never see it: they address this entry by its slug, so a server can be renamed on the robot without a single app changing.
doc.actions.*.type string yes The action type ros_name implements, with the action segment in the middle — nav2_msgs/action/NavigateToPose, never nav2_msgs/NavigateToPose. Declared rather than introspected, so an action can be configured for a robot that has never connected; the cloud checks it against the robot’s own definitions only once one is there.
doc.actions.*.message unknown no The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.actions.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.actions.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.actions.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.actions.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.actions.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.actions.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.actions.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.actions.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.actions.*.description string no What this action does, in the developer’s own words — documentation for the console and for MCP clients, which is all it is: the robot does nothing with it. It is carried verbatim into robot_describe and read by a model that has never seen this robot. The action is offered whenever the role grants it; without one it is offered with description: null, and the model has nothing but the slug.
doc.services record<string, object> no ROS service calls the robot answers — one request, one reply. Unlike an action a service reports no progress and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused busy, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.services.*.ros_name string yes The ROS service the robot answers on, as an absolute graph name. The call is one request and one reply with no progress in between, so whatever this service does has to finish inside that reply; anything long-running belongs in actions.
doc.services.*.type string yes The service type ros_name implements, with the srv segment in the middle — std_srvs/srv/Trigger. A type whose request has no fields, like Trigger, needs neither message nor parameters: there is nothing to fill.
doc.services.*.message unknown no The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.services.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.services.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.services.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.services.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.services.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.services.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.services.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.services.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.services.*.description string no What this service does, in the developer’s own words. The robot does nothing with it — the readers are the console and MCP clients, and without one the service is still offered, with description: null, exactly as for an action. It sits on the configuration rather than on the app, so one wording is true for every app that reaches this robot.
doc.publishers record<string, object> no Topics clients may send to, and where the format’s whole safety story lives. The message template fixes every value a caller cannot change, and failsafe is required: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.publishers.*.topic string yes The ROS topic the message is published onto, as an absolute graph name. No client ever names a topic: a caller addresses this entry by its slug, so the topics an app can write to are exactly the ones written in this file.
doc.publishers.*.type string yes The message type of topic, spelled the way ROS 2 spells it, with the msg segment. It fixes the shape that message and failsafe.message must both fill, which is why one publisher carries one type and a second type needs a second publisher.
doc.publishers.*.message unknown yes The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.publishers.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.publishers.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.publishers.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.publishers.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.publishers.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.publishers.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.publishers.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.publishers.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.publishers.*.failsafe object yes What the bridge sends by itself once a client stops sending, and how long it waits first. This is the format’s safety story in one field: a client that crashes, loses its connection or whose operator closes the window does not leave a robot driving. The message may hold no placeholder, inline or through a shared message — there is nobody left to fill one.
doc.publishers.*.failsafe.timeout_ms integer yes How long the bridge waits for the client’s next send before sending the failsafe message itself, in milliseconds. The deadline runs on the robot, so it still fires when the link to the cloud is what failed — which is the case it exists for.
doc.publishers.*.failsafe.message unknown yes What the bridge sends once timeout_ms runs out — for a drive command, a zero twist. It must be safe in every state, because it is sent precisely when nobody is watching any more, and it may hold no placeholder: there is no caller left to fill one.
doc.publishers.*.quiet_timeout_ms integer yes How long this publisher must stay silent before a different user may send to it. Whoever sends holds it implicitly exclusive, with no session and no lock, so this one number is the whole handover policy: too short and two operators fight over one robot, too long and a crashed client blocks it for everyone.
doc.publishers.*.description string no What sending to this publisher does, in the developer’s own words. It is documentation for the console and for MCP clients — the robot does nothing with it — and as for actions and services, the publisher is offered whether or not one is written, with description: null when it is not. A caller sends here repeatedly and continuously rather than once, which is why this kind alone carries failsafe and quiet_timeout_ms.
doc.cameras record<string, object> no Video the robot streams, and the still frames the cloud serves from it. width, height, fps and bitrate_kbps are what the bridge produces before sending, not what the camera captures — they live in the configuration rather than in a viewer’s request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.cameras.*.source object yes Where this camera’s frames come from. kind picks one of four sources and fixes which other fields the source may carry, so an impossible camera is unrepresentable rather than merely invalid — there is no way to write an RTSP camera with a ROS topic.
doc.cameras.*.source.kind "ros" yes Selects the ROS image-topic source: this camera then carries topic and type, and no field of another kind.
doc.cameras.*.source.topic string yes The ROS image topic the bridge subscribes to, as an absolute graph name. Clients never name it — they address the camera by its slug — so the topic can be renamed on the robot without an app changing.
doc.cameras.*.source.type string yes The message type of topic: sensor_msgs/msg/Image for raw frames, sensor_msgs/msg/CompressedImage for a camera that already encodes. Declared here rather than introspected, so a camera can be configured for a robot that has never connected.
doc.cameras.*.source.kind "rtsp" yes Selects the RTSP source: this camera then carries url, and optionally transport and credentials.
doc.cameras.*.source.url string yes Where the stream lives, reached from the robot rather than from the cloud. rtsp:// or rtsps:// only — the bridge opens this with a library that would equally honour file:, so an unconstrained URL would turn a configuration document into arbitrary file access on the robot. The bridge re-checks the scheme itself, so a validator that changed could not make a robot serve files.
doc.cameras.*.source.transport "tcp" | "udp" no How the RTSP payload is carried. Omitted means tcp: udp loses frames on a congested link and loses them silently, so the result looks like a failing camera rather than like a choice made here.
doc.cameras.*.source.credentials object no Username and password for the stream, standing in clear text in the document. A published version is immutable, so a password here cannot be removed from history or rotated without republishing — which is why the publish audit event carries only the version number and never the document body. Userinfo in the url works too; an explicit block here wins over it.
doc.cameras.*.source.credentials.username string no The account name the camera expects. For MJPEG the bridge sends a real HTTP Authorization: Basic header and leaves the URL untouched. RTSP offers no such channel through ffmpeg, so there the name goes inside the connect URL instead — built fresh for that one call and never written back into the stored document.
doc.cameras.*.source.credentials.password string no The password for username. There is no secret store behind this: the value written here is the value stored, so treat it as readable by everyone who may read this robot’s configuration, now and in its history.
doc.cameras.*.source.kind "mjpeg" yes Selects the MJPEG-over-HTTP source: this camera then carries url, and optionally credentials.
doc.cameras.*.source.url string yes Where the stream lives. http:// or https:// only — as for the rtsp URL, the bridge opens it with a library that would also serve file:. Plain http:// is permitted because these cameras usually sit on the robot’s own network, but Basic credentials on such a URL then travel in the clear.
doc.cameras.*.source.credentials object no Username and password for the stream, standing in clear text in the document. A published version is immutable, so a password here cannot be removed from history or rotated without republishing — which is why the publish audit event carries only the version number and never the document body. Userinfo in the url works too; an explicit block here wins over it.
doc.cameras.*.source.credentials.username string no The account name the camera expects. For MJPEG the bridge sends a real HTTP Authorization: Basic header and leaves the URL untouched. RTSP offers no such channel through ffmpeg, so there the name goes inside the connect URL instead — built fresh for that one call and never written back into the stored document.
doc.cameras.*.source.credentials.password string no The password for username. There is no secret store behind this: the value written here is the value stored, so treat it as readable by everyone who may read this robot’s configuration, now and in its history.
doc.cameras.*.source.kind "v4l2" yes Selects the local capture-device source: this camera then carries device and nothing else.
doc.cameras.*.source.device string yes The capture device, resolved on the robot and never by the cloud; a /dev/v4l/by-id/... symlink survives a reboot that renumbers /dev/video0. Constrained to /dev/ — the string reaches OpenCV, which will just as happily open an ordinary video file or an http:// URL and publish its pixels to the cloud. The bridge re-derives the same constraint rather than trusting the wire.
doc.cameras.*.width integer yes The width the bridge scales frames to before sending, in pixels — what the bridge produces, not what the sensor captures; a snapshot can arrive narrower, since the bridge reduces both dimensions together to fit its JPEG byte ceiling. It stands in the configuration and never in a viewer’s request, so no client can make the robot encode a larger frame than the developer allowed.
doc.cameras.*.height integer yes The height the bridge scales every frame to, in pixels; with width it is the size the live stream carries. A snapshot can arrive smaller than this — its JPEG has a byte ceiling, and the bridge gives up quality first and then resolution to fit, reporting the size it actually encoded.
doc.cameras.*.fps integer yes How many frames a second the bridge forwards, at most. It is a ceiling, not a clock: a camera that delivers ten frames a second stays at ten. Both modes read the same throttled pipeline, so this also bounds how fresh a snapshot can be.
doc.cameras.*.bitrate_kbps integer yes The ceiling for the live encoding, in kilobits per second — this is what bounds a watched camera against the robot’s uplink. Snapshots are not covered by it: they are JPEGs under their own byte ceiling. Raising width, height or fps against a fixed bitrate buys blur, not detail.
doc.cameras.*.snapshot_interval_seconds integer yes How often a still frame is captured, in seconds. It runs whether or not anyone is watching, unlike the live stream, which the cloud refcounts — first viewer starts it, last one ends it. The cloud caches the one frame and serves every reader from it, so a hundred pollers cost the robot exactly one image per interval.
doc.cameras.*.description string no What this camera shows, in the developer’s own words — documentation for whoever reads the configuration, for the console and for MCP clients; the robot does nothing with it. A camera without one is still offered, with description: null, as for actions, services and publishers. What camera_snapshot serves is the latest snapshot with its age; a live session is never a tool.
doc.low_bandwidth object no Overrides for the bridge’s low-bandwidth mode; see the section schema.
doc.low_bandwidth.mode "auto" | "on" | "off" no auto decides from the measured lag; on and off force the mode, for tests and for an operator who knows the link.
doc.low_bandwidth.enter_lag_ms integer no Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge’s parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.
doc.low_bandwidth.enter_after_s integer no The entry condition must hold this long.
doc.low_bandwidth.exit_lag_ms integer no Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.
doc.low_bandwidth.exit_after_s integer no The exit condition must hold this long.
doc.low_bandwidth.datapoint_max_hz number no The long-run rate for every datapoint in the mode, unless the datapoint says low_bandwidth: keep. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.
doc.low_bandwidth.camera "reduce" | "stop" no What happens to a running stream in the mode. New streams are refused either way.
doc.low_bandwidth.camera_bitrate_kbps integer no Bitrate applied to running streams under reduce.
source string yes
updated_at string | null yes
issues object[] yes
issues[].path string yes
issues[].slug string | null yes
issues[].code string yes
issues[].message string yes
issues[].severity "error" | "warning" yes

config-version-response

Field Type Required Description
version integer yes
published_at string yes
doc object yes
doc.fleetless 1 yes The format version, and the first line of the file. It decides how everything below is read, so a file that omits it — or names a version this cloud does not know — is refused rather than half understood.
doc.messages record<string, unknown> no Reusable message bodies, keyed by name, inserted elsewhere by writing ${name} directly after message:. A shared body may hold placeholders and whoever inserts it declares the parameters, so two publishers can send the same message under different bounds. A shared message may not insert another, so a ${name} inside a body is always a parameter and never a second message.
doc.datapoints record<string, object> no Values the robot publishes, each one field of one topic or a whole topic, and never several topics. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.datapoints.*.topic string yes The ROS topic this datapoint reads, as an absolute graph name. One datapoint reads one topic: a value assembled from two topics is not expressible here.
doc.datapoints.*.type string yes The message type carried by topic, spelled the way ROS 2 spells it, with the msg segment in the middle — sensor_msgs/msg/BatteryState, never sensor_msgs/BatteryState. It is declared here rather than discovered, so a configuration can be written for a robot that has never been connected; the cloud checks it against the robot’s own message definitions only once one is there.
doc.datapoints.*.field string no A dotted path into the message naming the single value this datapoint carries, each segment indexing at most one array level — ranges[0], never ranges[0][1], because ROS 2 has no nested arrays. Without it the datapoint is the whole message, and numeric, chart and alerts are then refused.
doc.datapoints.*.rate_throttle_hz number no A ceiling on how often this datapoint is sent, in hertz. Omitted or 0 means no throttling. It is a ceiling, not a clock: a slow topic stays slow, a value is never repeated to manufacture a rate, and within a window the newest value wins. The bridge enforces it, so the robot’s bandwidth is genuinely saved.
doc.datapoints.*.low_bandwidth "keep" no keep exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.
doc.datapoints.*.description string no Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into robot_describe, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with description: null and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.
doc.datapoints.*.numeric object no Arithmetic and formatting for a numeric value. scale and offset are applied on the robot, before sending, which is why REST, realtime and history all carry identical numbers. unit and decimals change nothing the robot does, so a publish that touches only those pushes no configuration.
doc.datapoints.*.numeric.scale number no A factor the robot multiplies the raw value by before sending it (value * scale + offset). The arithmetic happens once, at the source, so REST, realtime and history can never disagree about a number.
doc.datapoints.*.numeric.offset number no A constant the robot adds after scale (value * scale + offset), for a value whose zero sits in the wrong place. Like scale it is applied before sending, so history stores the converted value and a later correction cannot reach what is already stored.
doc.datapoints.*.numeric.unit string no The unit of the value after scale and offset, not the robot’s own. It is shown beside the value and carried by robot_describe as its own field, so a model does not have to guess whether 15 means percent, volts or minutes.
doc.datapoints.*.numeric.decimals integer no How many fraction digits the console shows the value with — value tile, chart axis and tooltip, and the datapoint detail page — and the number robot_describe reports as its own field, so a model formats the value the way the console does. Presentation only: the stored value keeps the precision it arrived with, and absent means the console’s own default rather than zero.
doc.datapoints.*.retention object no What outlives the moment: whether this value is written to the time series, how often, and how many points the robot buffers while the bridge is away. Absent means no history at all — the value is live only.
doc.datapoints.*.retention.enabled boolean no Whether values are written to the time series and become queryable. Off by default: without it the value is live only, and nobody who was not watching will ever see it.
doc.datapoints.*.retention.interval_seconds integer no How often a value is written to history, in seconds; absent means 300. Not how often it is sent — that is rate_throttle_hz. Stored points are billed, so this is the direct lever on what a robot costs, and a bumper that is true for 200 ms does not appear unless a write falls inside it.
doc.datapoints.*.retention.max_buffer_values integer no How many values the robot holds while the bridge is disconnected, to be pushed once it reconnects. The catch-up runs behind live telemetry and job results at a limited rate, so closing a gap never delays the present; without it the series simply has a gap, which is an honest answer.
doc.datapoints.*.chart object no How the console draws this value over time: axis bounds, whether the line interpolates or steps, and the window a chart opens on. Display only — it changes no stored value, no alert and nothing the robot does, so a publish that touches only it pushes no configuration.
doc.datapoints.*.chart.y_min number no A fixed floor for the chart’s y axis; omitted, the axis scales to the data. 0 is a real floor and is read as 0, never as unset.
doc.datapoints.*.chart.y_max number no A fixed ceiling for the chart’s y axis; omitted, the axis scales to the data. It may not sit below y_min: a reversed pair is refused here because nothing downstream catches it, and the chart would render empty.
doc.datapoints.*.chart.style "line" | "step" no How the drawing joins two samples, which is not a matter of taste. line claims the value moved evenly between them, roughly true of a temperature or a charge; step holds and then jumps, the only honest drawing for a mode, a switch or a counter, where a straight line would show values that never existed.
doc.datapoints.*.chart.default_window_minutes integer no How far back the chart reaches when it is first opened, in minutes; absent means 60. Only the starting zoom: a viewer may look further, and nothing about what is stored follows from it.
doc.datapoints.*.alerts record<string, object> no Alerts watching this value, keyed by slug; each moves between ok and firing and writes an org event on every transition. No mail is sent. The key is the identity, so renaming an alert is a delete plus a create: its runtime state is lost, and an alert that is still true fires again.
doc.datapoints.*.alerts.*.condition object yes When this alert fires and when it is ok again. It carries no discriminator: upper threshold, lower threshold or equality all follow from the two values in it. Editing it resets the alert to ok on the next publish, while a publish that leaves it untouched keeps the running state.
doc.datapoints.*.alerts.*.condition.fire_at number | string | boolean yes The value at which the alert starts firing. Alone it is an equality: it fires while the value equals fire_at and is ok again as soon as it differs, which is what makes a boolean or a string condition meaningful. Adding resolve_at turns it into a threshold instead.
doc.datapoints.*.alerts.*.condition.resolve_at number no The value at which a firing alert becomes ok again — allowed only when fire_at is a number, and it must differ from it. That gap is the hysteresis, and it makes the condition a threshold whose direction follows from which of the two values is higher. Without a gap a value sitting on the line flips on every sample.
doc.datapoints.*.alerts.*.severity "warning" | "error" no How bad it is when this alert fires; absent means warning. It changes no behaviour — nothing is escalated, retried or delivered differently — it travels with the org event and colours the alert wherever it is shown.
doc.datapoints.*.alerts.*.name string no A human-readable label shown wherever this alert appears, in place of its bare key. It is not the alert’s identity — the key is — so the label can be reworded freely, while changing the key deletes one alert and creates another.
doc.datapoints.*.alerts.*.enabled boolean no Whether this alert is evaluated at all. Absent means on, the opposite of retention.enabled: an alert that is written down watches unless it is explicitly switched off, which is how one is silenced without losing the key that identifies it.
doc.actions record<string, object> no Things the robot does on request that take time, each reported as a job with progress. At most one job runs per action slug: a second call is refused busy, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.actions.*.ros_name string yes The action server on the robot, as an absolute graph name — this is what the bridge sends the goal to. Clients never see it: they address this entry by its slug, so a server can be renamed on the robot without a single app changing.
doc.actions.*.type string yes The action type ros_name implements, with the action segment in the middle — nav2_msgs/action/NavigateToPose, never nav2_msgs/NavigateToPose. Declared rather than introspected, so an action can be configured for a robot that has never connected; the cloud checks it against the robot’s own definitions only once one is there.
doc.actions.*.message unknown no The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.actions.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.actions.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.actions.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.actions.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.actions.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.actions.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.actions.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.actions.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.actions.*.description string no What this action does, in the developer’s own words — documentation for the console and for MCP clients, which is all it is: the robot does nothing with it. It is carried verbatim into robot_describe and read by a model that has never seen this robot. The action is offered whenever the role grants it; without one it is offered with description: null, and the model has nothing but the slug.
doc.services record<string, object> no ROS service calls the robot answers — one request, one reply. Unlike an action a service reports no progress and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused busy, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.services.*.ros_name string yes The ROS service the robot answers on, as an absolute graph name. The call is one request and one reply with no progress in between, so whatever this service does has to finish inside that reply; anything long-running belongs in actions.
doc.services.*.type string yes The service type ros_name implements, with the srv segment in the middle — std_srvs/srv/Trigger. A type whose request has no fields, like Trigger, needs neither message nor parameters: there is nothing to fill.
doc.services.*.message unknown no The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.services.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.services.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.services.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.services.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.services.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.services.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.services.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.services.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.services.*.description string no What this service does, in the developer’s own words. The robot does nothing with it — the readers are the console and MCP clients, and without one the service is still offered, with description: null, exactly as for an action. It sits on the configuration rather than on the app, so one wording is true for every app that reaches this robot.
doc.publishers record<string, object> no Topics clients may send to, and where the format’s whole safety story lives. The message template fixes every value a caller cannot change, and failsafe is required: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.publishers.*.topic string yes The ROS topic the message is published onto, as an absolute graph name. No client ever names a topic: a caller addresses this entry by its slug, so the topics an app can write to are exactly the ones written in this file.
doc.publishers.*.type string yes The message type of topic, spelled the way ROS 2 spells it, with the msg segment. It fixes the shape that message and failsafe.message must both fill, which is why one publisher carries one type and a second type needs a second publisher.
doc.publishers.*.message unknown yes The message as it will be sent, written out in full: literals are fixed, ${name} is a hole a caller fills, and a field written 0.0 is one no client can change. Directly after message: a ${name} standing alone names a shared message instead; anywhere inside a body it is a parameter. null is refused at every depth — omitting a key is the only spelling of “not set”.
doc.publishers.*.parameters record<string, object> no The holes in this entry’s message that a caller fills, keyed by parameter name rather than by field path — so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every ${name} in the message must be declared; either half alone is an error.
doc.publishers.*.parameters.*.type "bool" | "byte" | "char" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" | "string" | "wstring" yes The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it — float64, not double. It decides which other constraints are allowed at all: min_value and max_value need a numeric type, regex needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.
doc.publishers.*.parameters.*.default number | string | boolean no The value used when a caller omits this parameter: without a default the parameter is required, because the message cannot be built without it. It must itself satisfy min_value, max_value, enum and regex — a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked.
doc.publishers.*.parameters.*.min_value number no The lowest value a caller may send; numeric types only. It is enforced in the cloud, before anything reaches the robot — this is where a speed limit actually holds, rather than in the app that is supposed to respect it.
doc.publishers.*.parameters.*.max_value number no The highest value a caller may send; numeric types only, and it may not sit below min_value. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.
doc.publishers.*.parameters.*.enum string | number[] no The complete set of values a caller may send. Integer and string types only — never a float, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match type, and a default must be one of them.
doc.publishers.*.parameters.*.regex string no A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is not anchored, so [a-z]+ accepts any value that merely contains a lowercase run — a pattern meant to cover the whole value writes its own ^ and $.
doc.publishers.*.parameters.*.description string no What this parameter means, in the developer’s own words, and documentation only — the robot does nothing with it. It travels into the input schema robot_describe publishes for this call, beside the bounds, so type and the range say what the value is and this is the only place that says what it does.
doc.publishers.*.failsafe object yes What the bridge sends by itself once a client stops sending, and how long it waits first. This is the format’s safety story in one field: a client that crashes, loses its connection or whose operator closes the window does not leave a robot driving. The message may hold no placeholder, inline or through a shared message — there is nobody left to fill one.
doc.publishers.*.failsafe.timeout_ms integer yes How long the bridge waits for the client’s next send before sending the failsafe message itself, in milliseconds. The deadline runs on the robot, so it still fires when the link to the cloud is what failed — which is the case it exists for.
doc.publishers.*.failsafe.message unknown yes What the bridge sends once timeout_ms runs out — for a drive command, a zero twist. It must be safe in every state, because it is sent precisely when nobody is watching any more, and it may hold no placeholder: there is no caller left to fill one.
doc.publishers.*.quiet_timeout_ms integer yes How long this publisher must stay silent before a different user may send to it. Whoever sends holds it implicitly exclusive, with no session and no lock, so this one number is the whole handover policy: too short and two operators fight over one robot, too long and a crashed client blocks it for everyone.
doc.publishers.*.description string no What sending to this publisher does, in the developer’s own words. It is documentation for the console and for MCP clients — the robot does nothing with it — and as for actions and services, the publisher is offered whether or not one is written, with description: null when it is not. A caller sends here repeatedly and continuously rather than once, which is why this kind alone carries failsafe and quiet_timeout_ms.
doc.cameras record<string, object> no Video the robot streams, and the still frames the cloud serves from it. width, height, fps and bitrate_kbps are what the bridge produces before sending, not what the camera captures — they live in the configuration rather than in a viewer’s request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say {robot, slug} without naming a kind; bridge_state and robot_details are built-in, and history is reserved because GET …/jobs/history would shadow an action of that name; all three are refused when the document is validated.
doc.cameras.*.source object yes Where this camera’s frames come from. kind picks one of four sources and fixes which other fields the source may carry, so an impossible camera is unrepresentable rather than merely invalid — there is no way to write an RTSP camera with a ROS topic.
doc.cameras.*.source.kind "ros" yes Selects the ROS image-topic source: this camera then carries topic and type, and no field of another kind.
doc.cameras.*.source.topic string yes The ROS image topic the bridge subscribes to, as an absolute graph name. Clients never name it — they address the camera by its slug — so the topic can be renamed on the robot without an app changing.
doc.cameras.*.source.type string yes The message type of topic: sensor_msgs/msg/Image for raw frames, sensor_msgs/msg/CompressedImage for a camera that already encodes. Declared here rather than introspected, so a camera can be configured for a robot that has never connected.
doc.cameras.*.source.kind "rtsp" yes Selects the RTSP source: this camera then carries url, and optionally transport and credentials.
doc.cameras.*.source.url string yes Where the stream lives, reached from the robot rather than from the cloud. rtsp:// or rtsps:// only — the bridge opens this with a library that would equally honour file:, so an unconstrained URL would turn a configuration document into arbitrary file access on the robot. The bridge re-checks the scheme itself, so a validator that changed could not make a robot serve files.
doc.cameras.*.source.transport "tcp" | "udp" no How the RTSP payload is carried. Omitted means tcp: udp loses frames on a congested link and loses them silently, so the result looks like a failing camera rather than like a choice made here.
doc.cameras.*.source.credentials object no Username and password for the stream, standing in clear text in the document. A published version is immutable, so a password here cannot be removed from history or rotated without republishing — which is why the publish audit event carries only the version number and never the document body. Userinfo in the url works too; an explicit block here wins over it.
doc.cameras.*.source.credentials.username string no The account name the camera expects. For MJPEG the bridge sends a real HTTP Authorization: Basic header and leaves the URL untouched. RTSP offers no such channel through ffmpeg, so there the name goes inside the connect URL instead — built fresh for that one call and never written back into the stored document.
doc.cameras.*.source.credentials.password string no The password for username. There is no secret store behind this: the value written here is the value stored, so treat it as readable by everyone who may read this robot’s configuration, now and in its history.
doc.cameras.*.source.kind "mjpeg" yes Selects the MJPEG-over-HTTP source: this camera then carries url, and optionally credentials.
doc.cameras.*.source.url string yes Where the stream lives. http:// or https:// only — as for the rtsp URL, the bridge opens it with a library that would also serve file:. Plain http:// is permitted because these cameras usually sit on the robot’s own network, but Basic credentials on such a URL then travel in the clear.
doc.cameras.*.source.credentials object no Username and password for the stream, standing in clear text in the document. A published version is immutable, so a password here cannot be removed from history or rotated without republishing — which is why the publish audit event carries only the version number and never the document body. Userinfo in the url works too; an explicit block here wins over it.
doc.cameras.*.source.credentials.username string no The account name the camera expects. For MJPEG the bridge sends a real HTTP Authorization: Basic header and leaves the URL untouched. RTSP offers no such channel through ffmpeg, so there the name goes inside the connect URL instead — built fresh for that one call and never written back into the stored document.
doc.cameras.*.source.credentials.password string no The password for username. There is no secret store behind this: the value written here is the value stored, so treat it as readable by everyone who may read this robot’s configuration, now and in its history.
doc.cameras.*.source.kind "v4l2" yes Selects the local capture-device source: this camera then carries device and nothing else.
doc.cameras.*.source.device string yes The capture device, resolved on the robot and never by the cloud; a /dev/v4l/by-id/... symlink survives a reboot that renumbers /dev/video0. Constrained to /dev/ — the string reaches OpenCV, which will just as happily open an ordinary video file or an http:// URL and publish its pixels to the cloud. The bridge re-derives the same constraint rather than trusting the wire.
doc.cameras.*.width integer yes The width the bridge scales frames to before sending, in pixels — what the bridge produces, not what the sensor captures; a snapshot can arrive narrower, since the bridge reduces both dimensions together to fit its JPEG byte ceiling. It stands in the configuration and never in a viewer’s request, so no client can make the robot encode a larger frame than the developer allowed.
doc.cameras.*.height integer yes The height the bridge scales every frame to, in pixels; with width it is the size the live stream carries. A snapshot can arrive smaller than this — its JPEG has a byte ceiling, and the bridge gives up quality first and then resolution to fit, reporting the size it actually encoded.
doc.cameras.*.fps integer yes How many frames a second the bridge forwards, at most. It is a ceiling, not a clock: a camera that delivers ten frames a second stays at ten. Both modes read the same throttled pipeline, so this also bounds how fresh a snapshot can be.
doc.cameras.*.bitrate_kbps integer yes The ceiling for the live encoding, in kilobits per second — this is what bounds a watched camera against the robot’s uplink. Snapshots are not covered by it: they are JPEGs under their own byte ceiling. Raising width, height or fps against a fixed bitrate buys blur, not detail.
doc.cameras.*.snapshot_interval_seconds integer yes How often a still frame is captured, in seconds. It runs whether or not anyone is watching, unlike the live stream, which the cloud refcounts — first viewer starts it, last one ends it. The cloud caches the one frame and serves every reader from it, so a hundred pollers cost the robot exactly one image per interval.
doc.cameras.*.description string no What this camera shows, in the developer’s own words — documentation for whoever reads the configuration, for the console and for MCP clients; the robot does nothing with it. A camera without one is still offered, with description: null, as for actions, services and publishers. What camera_snapshot serves is the latest snapshot with its age; a live session is never a tool.
doc.low_bandwidth object no Overrides for the bridge’s low-bandwidth mode; see the section schema.
doc.low_bandwidth.mode "auto" | "on" | "off" no auto decides from the measured lag; on and off force the mode, for tests and for an operator who knows the link.
doc.low_bandwidth.enter_lag_ms integer no Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge’s parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.
doc.low_bandwidth.enter_after_s integer no The entry condition must hold this long.
doc.low_bandwidth.exit_lag_ms integer no Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.
doc.low_bandwidth.exit_after_s integer no The exit condition must hold this long.
doc.low_bandwidth.datapoint_max_hz number no The long-run rate for every datapoint in the mode, unless the datapoint says low_bandwidth: keep. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.
doc.low_bandwidth.camera "reduce" | "stop" no What happens to a running stream in the mode. New streams are refused either way.
doc.low_bandwidth.camera_bitrate_kbps integer no Bitrate applied to running streams under reduce.
source string yes

config-versions-response

Field Type Required Description
versions object[] yes
versions[].version integer yes
versions[].published_at string yes

create-app-invitation-request

Field Type Required Description
email string yes The address to invite. An invitation always bypasses the domain whitelist — a developer inviting somebody by hand has already made the decision the whitelist automates.
role_id string no The role the invitee gets on acceptance. Absent means the app’s default_role_id.
display_name string | null no An optional name to pre-fill the account with; the invitee can change it afterwards.
send_mail boolean yes Whether Fleetless mails the invitation. The link points at the app’s invite_url, or at the Fleetless-hosted invitation page when the app has configured none — so the mail always leads somewhere, and nothing is refused for a missing URL.

create-app-oidc-provider-request

Field Type Required Description
slug string yes The handle for this provider, unique per app. Immutable once created — identities are keyed by it, so a rename would orphan every linked account.
name string yes What the developer’s sign-in page calls this provider.
issuer string yes The provider’s issuer URL. Shape-checked here (http(s), no credentials, query or fragment); the authoritative SSRF defence is at the discovery fetch.
client_id string yes The OAuth client registered at the provider for Fleetless.
client_secret string yes The client secret, write-only: it is stored encrypted and comes back through nothing — not the read, not this route’s own answer, not an audit detail. Required on create, since a provider with no secret cannot exchange a code; the minimum length refuses a value that is a misconfiguration rather than a secret.
scopes string[] no The scopes to request. Defaults to openid email profile, which is what account linking actually reads: the subject, the address and its verified flag, and a name.
link_verified_emails boolean no Whether a federated login may join an existing app user by verified address. Defaults to off, because relaxing later is additive and admitting duplicates now and tightening afterwards is not.
enabled boolean no Whether the provider is offered immediately. Defaults to on: a developer who just typed a client secret is configuring a provider they mean to use.

create-app-request

Field Type Required Description
name string yes
identifier string yes
robot_ids string[] no

create-app-user-request

Field Type Required Description
email string yes The address, unique per app case-insensitively. An address this app already knows is refused with email_taken — a developer-authenticated route may say so, unlike the public registration route.
password string yes The initial password. At least 12 characters: length only, because a rule a user cannot predict is a rule they work around.
display_name string | null no Optional human name. Absent leaves it unset; an explicit null is the same end state.
role_id string no The role the new user holds. Absent means the app’s default_role_id, and 409 target_state_conflict when the app has none or its default no longer resolves; a role belonging to another app is 404 not_found, the same refusal a role that never existed gets.

create-passkey-request

Field Type Required Description
name string yes What to call the passkey, such as the device it lives on.
credential record<string, unknown> yes The browser’s RegistrationResponseJSON for the options POST /api/auth/passkeys/options answered.

create-passkey-response

Field Type Required Description
passkey object yes The passkey as it is now stored.
passkey.id string yes The passkey in the API, as renamed and removed through /api/auth/passkeys/:id.
passkey.name string yes What the person called it, such as the device it lives on.
passkey.created_at string yes When it was registered.
passkey.last_used_at string | null yes When it last signed the person in or confirmed a sign-in, or null if never.
passkey.synced boolean | null yes Whether the authenticator reported the passkey as syncable across the person’s devices (the backup-eligible flag), or null when it said nothing.
recovery_codes string[] | null yes The ten recovery codes, shown once, when this passkey is the account’s first second factor; null when the account already had one and its codes stay valid.

create-robot-request

Field Type Required Description
name string yes

create-robot-response

Field Type Required Description
robot object yes
robot.id string yes The robot, and what every robot-scoped route takes as its :id.
robot.name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
robot.created_at string yes When the robot was created, as an ISO 8601 timestamp.
token string yes

create-server-key-response

Field Type Required Description
server_key object yes
server_key.id string yes The key row, and what the rotate and delete routes address. It is not the key: this shape never carries the secret.
server_key.app_id string yes The app whose full rights this key carries. A key is never shared between apps.
server_key.name string yes A label the developer chose, so a key can be recognised before it is rotated or deleted.
server_key.created_at string yes When the key was minted, as an ISO 8601 timestamp. GET /api/apps/:id/server-keys orders by this field.
server_key.last_used_at string | null yes When this key last authenticated a request, or null if it never has — the cheapest way to spot a key nobody needs.
key string yes

create-team-invite-request

Field Type Required Description
email string yes The address to invite. Globally unique across Fleetless users, so an address already on another org’s team is refused with email_taken.
display_name string | null no An optional name to pre-fill the account with; the invitee can change it afterwards.
tier "owner" | "developer" yes The tier the invitee holds on acceptance. Required, not defaulted — “I did not think about it” and “I meant developer” would otherwise be the same request, on the field that decides who can remove whom. Inviting an owner requires the owner tier.
send_mail boolean yes Whether Fleetless mails the invitation. The link in the answer is the primary path either way: a deployment with no SMTP still issues invitations and says so through mail.

datapoint-event

Field Type Required Description
type "datapoint" yes
robot_id string yes
slug string yes
value unknown yes
timestamp_ms integer yes

datapoint-list-response

Field Type Required Description
datapoints object[] yes Everything a client may read on this robot: the two built-ins, plus every datapoint the published configuration exposes and the caller’s role grants.
datapoints[].slug string yes The name a client reads this datapoint by.
datapoints[].builtin boolean yes true for the datapoints every robot has — bridge_state and robot_details — and false for everything the published configuration adds.
datapoints[].unit string | null yes The unit the value carries after any scale and offset, shown beside the number so nobody has to guess whether 15 means percent, volts or minutes. null when the configuration names none.
datapoints[].rate_throttle_hz number | null yes The ceiling on how often this datapoint is sent, in hertz. null means no ceiling is configured, which is also the answer for every built-in. A ceiling, not a clock: a slow topic stays slow and no value is repeated to manufacture a rate.

datapoint-value

Field Type Required Description
slug string yes The datapoint this value belongs to.
value unknown yes The value, shaped by the datapoint: a number, a boolean, a string, or the whole ROS message where the configuration names no field inside it. Any scale and offset the configuration declares have already been applied, at the robot.
timestamp_ms integer yes When the value was captured, as a unix timestamp in milliseconds. The bridge’s capture time, never the time the cloud received it — the one exception is the built-in bridge_state, which the cloud observes by construction.

developer-passkey

Field Type Required Description
id string yes The passkey in the API, as renamed and removed through /api/auth/passkeys/:id.
name string yes What the person called it, such as the device it lives on.
created_at string yes When it was registered.
last_used_at string | null yes When it last signed the person in or confirmed a sign-in, or null if never.
synced boolean | null yes Whether the authenticator reported the passkey as syncable across the person’s devices (the backup-eligible flag), or null when it said nothing.

developer-two-factor

Field Type Required Description
passkeys object[] yes Every passkey the caller has registered, oldest first. Empty when none.
passkeys[].id string yes The passkey in the API, as renamed and removed through /api/auth/passkeys/:id.
passkeys[].name string yes What the person called it, such as the device it lives on.
passkeys[].created_at string yes When it was registered.
passkeys[].last_used_at string | null yes When it last signed the person in or confirmed a sign-in, or null if never.
passkeys[].synced boolean | null yes Whether the authenticator reported the passkey as syncable across the person’s devices (the backup-eligible flag), or null when it said nothing.
authenticator object | null yes The confirmed authenticator app, or null when there is none. At most one.
authenticator.created_at string yes When the authenticator was confirmed.
recovery_codes_left integer yes How many of the ten recovery codes are unspent. 0 while the caller has no second factor.
required_by_org boolean yes Whether the organisation requires a second factor. While it does, the last one cannot be removed.

dynamic-client-registration-request

What an MCP client sends to register itself, per RFC 7591. Unknown metadata is ignored rather than refused (§3.1), and the answer states what was actually granted rather than what was asked for (§3.2.1).

Field Type Required Description
redirect_uris string[] yes Where the authorization code may be returned, and the one field a registration cannot omit. Each must be an https URL, or http on an explicit loopback address for a native app that cannot hold a certificate, and none may carry a fragment. Between 1 and 5 of them; duplicates are collapsed rather than counted twice. Matched exactly at the authorize step against what was registered here.
client_name string no The name the client calls itself. Optional: RFC 7591 makes every metadata field optional, so a registration without one is recorded under a default name. It is not vouched for by Fleetless and must never be rendered as if it were: a self-registered client chooses this string, and one has called itself “Fleetless Official Helper”.
token_endpoint_auth_method "none" no none, RFC 7591’s value for a public client, and the only value either server registers. Any other value is refused rather than silently downgraded: a client that believes it holds a secret and does not has a wrong mental model of its own security. There is no client secret to hold — mandatory PKCE (S256) is the defence.
grant_types "authorization_code" | "refresh_token"[] no Accepted for conformance with RFC 7591 and then ignored: both MCP authorization servers grant authorization_code and refresh_token to every registration, and the answer states what was granted (§3.2.1) rather than what was asked.
response_types "code"[] no Accepted for conformance and then ignored; the response names code, which is the only response type OAuth 2.1 leaves, the implicit grant having been removed.
scope string no Accepted for conformance and then ignored. This authorization server issues no scopes at all, which is why the registration answer carries no scope field to echo one back in.

dynamic-client-registration-response

Field Type Required Description
client_id string yes The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier.
client_name string yes The name the client registered under, echoed back. Chosen by the client and not vouched for by Fleetless.
redirect_uris string[] yes The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else.
grant_types string[] yes The grants this client may use. Always exactly ["authorization_code", "refresh_token"] — an exchange mints a refresh token and the token endpoint rotates it.
response_types string[] yes The response types this client may ask for: code.
token_endpoint_auth_method "none" yes none — this server registers public clients only, and PKCE rather than a secret is what protects the exchange.
client_id_issued_at integer yes When the registration was created, in seconds since the epoch, per RFC 7591.
client_secret_expires_at 0 yes Always 0, which is RFC 7591’s way of saying the client secret never expires — there is none. The registration itself does expire: a self-registered client that never completes a flow is an unauthenticated write somebody left behind.

exposure-list-response

Field Type Required Description
exposures object[] yes
exposures[].slug string yes
exposures[].kind "datapoint" | "action" | "service" | "publisher" | "camera" yes
exposures[].builtin boolean yes

feedback-request

Field Type Required Description
kind "idea" | "problem" | "question" | "other" yes What the message is: an idea, a problem, a question or other. It only sorts the inbox; it changes nothing about how the message is handled.
message string yes What the developer wrote, trimmed. At most 5000 characters; a message that is only whitespace is refused.
page string yes The console path the message was sent from, e.g. /robots/:id/jobs with its real id. A path, never a full URL, so no host and no query string reach the inbox by accident.

feedback-response

Field Type Required Description
id string yes The stored message. It exists whatever mail says.
mail "sent" | "not_requested" | "not_configured" | "failed" yes What happened to the notification mail: sent, failed, or not_configured when this cloud has no feedback address. The message is stored in every case, so a client shows success for all three.

fetch-types-request

Field Type Required Description
type_names string[] yes

fetch-types-response

Field Type Required Description
types object[] yes
types[].name string yes
types[].kind "msg" yes
types[].fields unknown[] yes
types[].name string yes
types[].kind "srv" yes
types[].request unknown[] yes
types[].response unknown[] yes
types[].name string yes
types[].kind "action" yes
types[].goal unknown[] yes
types[].result unknown[] yes
types[].feedback unknown[] yes
unresolved string[] yes

fleetless-user

Field Type Required Description
id string yes The Fleetless user in the API, assigned by the cloud and stable for the life of the account.
org_id string yes The organisation this person belongs to. Every developer route is already scoped to the caller’s org, so this confirms what a client is looking at, not a filter it applies.
email string yes The address the account is identified by, globally unique across every organisation. Immutable after creation: it is what every invitation, reset link and audit line names.
display_name string | null yes Optional human name, shown by the console instead of the address where present. Self-service through PATCH /api/auth/me; never used for authentication. null when the person never supplied one.
tier "owner" | "developer" yes The console powers this person holds. Required — every Fleetless user has a tier; it was optional only while the org also held people with no console powers to grade, and that pool is gone.
two_factor object yes The person’s second factors, as the team list shows them: none, passkeys, an authenticator, or both. No credential travels here. An owner resets them through DELETE /api/org/users/:id/two-factor.
two_factor.passkeys integer yes How many passkeys the person has registered.
two_factor.authenticator boolean yes Whether the person has a confirmed authenticator app.
created_at string yes When the account was created, as an ISO 8601 timestamp.

fleetless-user-list-response

Field Type Required Description
users object[] yes Every Fleetless user of the caller’s organisation. This is the team, not an app’s users — those are listed per app.
users[].id string yes The Fleetless user in the API, assigned by the cloud and stable for the life of the account.
users[].org_id string yes The organisation this person belongs to. Every developer route is already scoped to the caller’s org, so this confirms what a client is looking at, not a filter it applies.
users[].email string yes The address the account is identified by, globally unique across every organisation. Immutable after creation: it is what every invitation, reset link and audit line names.
users[].display_name string | null yes Optional human name, shown by the console instead of the address where present. Self-service through PATCH /api/auth/me; never used for authentication. null when the person never supplied one.
users[].tier "owner" | "developer" yes The console powers this person holds. Required — every Fleetless user has a tier; it was optional only while the org also held people with no console powers to grade, and that pool is gone.
users[].two_factor object yes The person’s second factors, as the team list shows them: none, passkeys, an authenticator, or both. No credential travels here. An owner resets them through DELETE /api/org/users/:id/two-factor.
users[].two_factor.passkeys integer yes How many passkeys the person has registered.
users[].two_factor.authenticator boolean yes Whether the person has a confirmed authenticator app.
users[].created_at string yes When the account was created, as an ISO 8601 timestamp.

history-buckets-response

Field Type Required Description
slug string yes The datapoint these buckets summarise.
kind "buckets" yes Says this is the aggregated shape, which the query asked for by naming a window. A separate shape rather than the sample shape with nulls in it, so a client knows by type what it received rather than by inspection.
window_ms integer yes The bucket width actually used, in milliseconds — the query’s window resolved to a number, so a rendered chart can say what it is drawing without re-parsing the expression it sent.
agg "min" | "max" | "avg" yes How each bucket reduced the samples inside it, echoed back from the query.
buckets object[] yes The buckets covering the queried window, oldest first. A range and window that would produce more than limit buckets is refused before the query runs, because this shape carries no truncated field and a refusal is then the only honest answer.
buckets[].bucket_start_ms integer yes The instant this bucket opens, as a unix timestamp in milliseconds. Buckets are half-open and window_ms wide, so this one covers up to but not including bucket_start_ms + window_ms.
buckets[].value number | null yes The aggregate over this bucket’s numeric samples, or null when none of them were numeric — which is not the same as the bucket being empty. sample_count separates those: null with a count of 0 is a gap a chart should draw as a break, null with a count above 0 is data that simply has no height.
buckets[].sample_count integer yes Every sample that landed in this bucket and inside the queried range, whether or not it contributed to value — only a count of all samples can prove a bucket empty rather than merely unplottable. Two consequences: value * sample_count is not a sum, and on a first or last bucket the count reflects the range rather than the bucket, so a low edge count is a boundary effect and not a quiet period.

history-query

Field Type Required Description
from string yes The start of the window: either a relative expression — now-30s, now-5m, now-1h — or absolute unix milliseconds. A chart asks the first way and a report asks the second, and making a client convert would be making it guess our clock.
to string no The end of the window, in the same two spellings as from; absent means now. The window is half-open, [from, to), so a sample landing exactly on to belongs to the next window and adjacent windows tile without double-counting.
window string no The bucket width, such as 10s or 1m. Absent means raw samples. It is meaningless without agg, and the pair is refused apart rather than defaulted — a silently chosen aggregation is a chart that lies quietly.
agg "min" | "max" | "avg" no How each bucket reduces the samples inside it. Valid only together with window.
field string no A dotted path to a numeric field inside an object value, such as pose.x. Without it the datapoint’s value is used whole, which only works when it is already a number.
limit string | integer no The most samples or buckets to return, from 1 to 10000. It arrives as text on the query string, so both a numeric string and a number are accepted; the ceiling is enforced after parsing rather than by the published shape.

history-response

Field Type Required Description
slug string yes The datapoint these samples belong to.
kind "samples" yes Says this is the raw-sample shape, which the query asked for by omitting window. A client reads this rather than inspecting which fields arrived.
samples object[] yes The samples in the queried window, oldest first. The window is half-open, [from, to), so a sample landing exactly on to belongs to the next window.
samples[].timestamp_ms integer yes When the sample was captured, as a unix timestamp in milliseconds. It is the bridge’s capture time — the same instant the live value carried, so a recorded point and a live one sit on one axis without apology.
samples[].value unknown yes The value as it was stored, shaped by the datapoint. A field in the query narrows a message down to one number; without one the whole stored value comes back.
truncated boolean yes Whether the response was cut short. A short array that does not admit it is indistinguishable from a quiet period, and the two lead a developer to opposite conclusions.
truncated_by "limit" | "bytes" | null yes Why it was cut, and null when it was not — the two causes have different remedies and one boolean cannot tell them apart. limit means too many rows, so raising limit helps. bytes means the rows are large, so raising limit changes nothing: narrow the range, or name a numeric field so whole messages are not carried.
slug string yes The datapoint these buckets summarise.
kind "buckets" yes Says this is the aggregated shape, which the query asked for by naming a window. A separate shape rather than the sample shape with nulls in it, so a client knows by type what it received rather than by inspection.
window_ms integer yes The bucket width actually used, in milliseconds — the query’s window resolved to a number, so a rendered chart can say what it is drawing without re-parsing the expression it sent.
agg "min" | "max" | "avg" yes How each bucket reduced the samples inside it, echoed back from the query.
buckets object[] yes The buckets covering the queried window, oldest first. A range and window that would produce more than limit buckets is refused before the query runs, because this shape carries no truncated field and a refusal is then the only honest answer.
buckets[].bucket_start_ms integer yes The instant this bucket opens, as a unix timestamp in milliseconds. Buckets are half-open and window_ms wide, so this one covers up to but not including bucket_start_ms + window_ms.
buckets[].value number | null yes The aggregate over this bucket’s numeric samples, or null when none of them were numeric — which is not the same as the bucket being empty. sample_count separates those: null with a count of 0 is a gap a chart should draw as a break, null with a count above 0 is data that simply has no height.
buckets[].sample_count integer yes Every sample that landed in this bucket and inside the queried range, whether or not it contributed to value — only a count of all samples can prove a bucket empty rather than merely unplottable. Two consequences: value * sample_count is not a sum, and on a first or last bucket the count reflects the range rather than the bucket, so a low edge count is a boundary effect and not a quiet period.

history-samples-response

Field Type Required Description
slug string yes The datapoint these samples belong to.
kind "samples" yes Says this is the raw-sample shape, which the query asked for by omitting window. A client reads this rather than inspecting which fields arrived.
samples object[] yes The samples in the queried window, oldest first. The window is half-open, [from, to), so a sample landing exactly on to belongs to the next window.
samples[].timestamp_ms integer yes When the sample was captured, as a unix timestamp in milliseconds. It is the bridge’s capture time — the same instant the live value carried, so a recorded point and a live one sit on one axis without apology.
samples[].value unknown yes The value as it was stored, shaped by the datapoint. A field in the query narrows a message down to one number; without one the whole stored value comes back.
truncated boolean yes Whether the response was cut short. A short array that does not admit it is indistinguishable from a quiet period, and the two lead a developer to opposite conclusions.
truncated_by "limit" | "bytes" | null yes Why it was cut, and null when it was not — the two causes have different remedies and one boolean cannot tell them apart. limit means too many rows, so raising limit helps. bytes means the rows are large, so raising limit changes nothing: narrow the range, or name a numeric field so whole messages are not carried.

introspection-response

Field Type Required Description
graph object yes
graph.topics object[] yes
graph.topics[].name string yes
graph.topics[].types string[] yes
graph.services object[] yes
graph.services[].name string yes
graph.services[].types string[] yes
graph.actions object[] yes
graph.actions[].name string yes
graph.actions[].types string[] yes
graph.captured_at_ms integer yes
fetched_at string yes
stale boolean yes

invoke-or-service-response

Field Type Required Description
job object yes The job that now exists on this slug. It is returned as soon as the goal is accepted, so state is running here — the outcome is observed afterwards, by slug, over polling or a subscription.
job.id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
job.robot_id string yes The robot this job is running on.
job.slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
job.state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
job.origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
job.started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
job.updated_at string yes When this job last changed, as an ISO 8601 timestamp.
job.seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
job.result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
job.error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
job.error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
job.error.message string yes A human-readable sentence saying what went wrong.
job.error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.
kind "action" | "service" yes Always action in this shape. A caller sends the same request for both kinds and cannot tell from a role grant which it invoked, so the answer says which it was rather than leaving it to be inferred from the shape.
result unknown yes What the service returned, shaped by the ROS service. A service call is awaited to completion, so there is no job to observe afterwards and no id to hold on to.

invoke-request

Field Type Required Description
params record<string, unknown> yes The values this call needs, keyed by parameter name rather than by field path — so a name survives the field moving inside the message. Every parameter without a default must be present, and the bounds the configuration declares are enforced in the cloud, before anything reaches the robot.
patience_ms integer no How long this call is worth waiting for, in milliseconds; absent means the platform default. The number travels to the robot too, so one deadline governs both sides. Above the maximum the call is refused rather than quietly clamped, because a caller given less than they asked for would read the timeout as the robot’s failure.

job

Field Type Required Description
id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
robot_id string yes The robot this job is running on.
slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
updated_at string yes When this job last changed, as an ISO 8601 timestamp.
seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
error.message string yes A human-readable sentence saying what went wrong.
error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.

job-event

Field Type Required Description
type "job" yes
robot_id string yes
slug string yes
job object yes
job.id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
job.robot_id string yes The robot this job is running on.
job.slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
job.state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
job.origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
job.started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
job.updated_at string yes When this job last changed, as an ISO 8601 timestamp.
job.seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
job.result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
job.error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
job.error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
job.error.message string yes A human-readable sentence saying what went wrong.
job.error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.
feedback unknown | null yes
progress number | null yes
timestamp_ms integer yes

job-response

Field Type Required Description
job object | null yes The most recent job on this slug, running or already finished, and null only when nothing has ever run there. Read state to tell a live job from a settled one: a route that forgot a job the moment it settled would let a poller see running and then nothing.
job.id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
job.robot_id string yes The robot this job is running on.
job.slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
job.state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
job.origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
job.started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
job.updated_at string yes When this job last changed, as an ISO 8601 timestamp.
job.seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
job.result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
job.error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
job.error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
job.error.message string yes A human-readable sentence saying what went wrong.
job.error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.

job-run

Field Type Required Description
id string yes The run’s id — the same id the invocation was answered with, so a caller that kept a job id can find its durable record here later.
robot_id string yes The robot the run happened on.
slug string yes The action or service that was invoked, as the published configuration exposed it at the time.
kind "action" | "service" yes Whether the slug was an action or a service.
state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes How the run ended, or running while it is still going. unknown while the robot has not accounted for it — offline or silent — and updated once the bridge says how it stands. lost is final: the bridge did not know the run and nothing else ran on its action, so the outcome is unknowable rather than unknown.
started_at string yes When the run started, as an ISO 8601 timestamp. Runs are listed and filtered by this instant.
ended_at string | null yes When the run finished, as an ISO 8601 timestamp. null while it is still running or unknown — a run has an end only once it has one.
duration_ms integer | null yes How long the run took, in milliseconds. null while it is still running or unknown, never 0 standing in for “nothing so far”.
result unknown | null yes What the action or service returned once it succeeded, shaped by ROS itself. null otherwise.
error object | null yes Why the run failed — a message, a code where one exists, and the structured details some codes carry. null unless it failed.
error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
error.message string yes A human-readable sentence saying what went wrong.
error.details unknown no The structured payload belonging to code, for the codes that document one. Absent for a failure with nothing structured to add.
actor object yes Who invoked the run, and what they were acting as at the time.
actor.kind "developer" | "end_user" | "app_user" | "server_key" yes What the caller was acting as: a developer in the console, an app_user of one app, or a server_key used by server-side code. A bridge invokes nothing, so it is deliberately not a case here. end_user appears only on runs recorded before app users replaced the organisation-wide user pool — kept so old runs still render; nothing writes it now.
actor.id string yes The id of the Fleetless user, app user or server key that invoked the run.
actor.label string yes A display name taken at invoke time — the email for a Fleetless user or an app user, the key’s own name for a server key. Storing it rather than joining is the point: renaming a key afterwards does not rewrite history.
actor.name string | null yes The person’s display name when the job started: the Fleetless user’s display_name for a developer, the app user’s display_name for an app user. null for a server key, when the person had no name set, and for runs recorded before contracts 5.3.0. Show label when it is null.
seq integer yes The durable cursor this history is ordered and paged by. Unlike job.seq it does not restart when the cloud does; it is the value a caller sends back as before_seq.
progress number | null yes How far a still-running run has got, as a fraction from 0 to 1, read live from the in-memory registry. null means not known right now — after a cloud restart, before the bridge reconnects — and never a 0 standing in for “no progress yet”.
feedback unknown | null yes The most recent action feedback for a run that is still running, shaped by the ROS action. Live-only, so it is null for every settled run and whenever the registry has nothing.

job-run-list-response

Field Type Required Description
runs object[] yes This page of runs, newest first by seq. Empty means the filter matched nothing, not that the history is gone.
runs[].id string yes The run’s id — the same id the invocation was answered with, so a caller that kept a job id can find its durable record here later.
runs[].robot_id string yes The robot the run happened on.
runs[].slug string yes The action or service that was invoked, as the published configuration exposed it at the time.
runs[].kind "action" | "service" yes Whether the slug was an action or a service.
runs[].state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes How the run ended, or running while it is still going. unknown while the robot has not accounted for it — offline or silent — and updated once the bridge says how it stands. lost is final: the bridge did not know the run and nothing else ran on its action, so the outcome is unknowable rather than unknown.
runs[].started_at string yes When the run started, as an ISO 8601 timestamp. Runs are listed and filtered by this instant.
runs[].ended_at string | null yes When the run finished, as an ISO 8601 timestamp. null while it is still running or unknown — a run has an end only once it has one.
runs[].duration_ms integer | null yes How long the run took, in milliseconds. null while it is still running or unknown, never 0 standing in for “nothing so far”.
runs[].result unknown | null yes What the action or service returned once it succeeded, shaped by ROS itself. null otherwise.
runs[].error object | null yes Why the run failed — a message, a code where one exists, and the structured details some codes carry. null unless it failed.
runs[].error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
runs[].error.message string yes A human-readable sentence saying what went wrong.
runs[].error.details unknown no The structured payload belonging to code, for the codes that document one. Absent for a failure with nothing structured to add.
runs[].actor object yes Who invoked the run, and what they were acting as at the time.
runs[].actor.kind "developer" | "end_user" | "app_user" | "server_key" yes What the caller was acting as: a developer in the console, an app_user of one app, or a server_key used by server-side code. A bridge invokes nothing, so it is deliberately not a case here. end_user appears only on runs recorded before app users replaced the organisation-wide user pool — kept so old runs still render; nothing writes it now.
runs[].actor.id string yes The id of the Fleetless user, app user or server key that invoked the run.
runs[].actor.label string yes A display name taken at invoke time — the email for a Fleetless user or an app user, the key’s own name for a server key. Storing it rather than joining is the point: renaming a key afterwards does not rewrite history.
runs[].actor.name string | null yes The person’s display name when the job started: the Fleetless user’s display_name for a developer, the app user’s display_name for an app user. null for a server key, when the person had no name set, and for runs recorded before contracts 5.3.0. Show label when it is null.
runs[].seq integer yes The durable cursor this history is ordered and paged by. Unlike job.seq it does not restart when the cloud does; it is the value a caller sends back as before_seq.
runs[].progress number | null yes How far a still-running run has got, as a fraction from 0 to 1, read live from the in-memory registry. null means not known right now — after a cloud restart, before the bridge reconnects — and never a 0 standing in for “no progress yet”.
runs[].feedback unknown | null yes The most recent action feedback for a run that is still running, shaped by the ROS action. Live-only, so it is null for every settled run and whenever the registry has nothing.
next_cursor integer | null yes The seq to send as before_seq to keep reading, or null when there is nothing further. null is a promise, not an observation — a caller who instead compares the page length against limit is wrong the moment a filter makes a page thin.

job-run-query

Field Type Required Description
before_seq string | integer no Return only runs with a seq below this value — the next, older page. Send back the next_cursor of the previous response rather than computing one.
limit string | integer no How many runs to return, from 1 to 200. Absent means 100. It arrives on the query string, so a numeric string and a number are both accepted.
robot_id string no Only runs on this robot. Absent means every robot in the organisation.
slug string no Only runs of this action or service.
state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" no Only runs in this state — running, unknown, succeeded, failed, cancelled or lost.
kind "action" | "service" no Only action runs, or only service runs.
from_ms string | integer no Only runs that started at or after this unix timestamp in milliseconds. Together with to_ms the window is half-open, [from, to), so adjacent windows tile without counting a run twice.
to_ms string | integer no Only runs that started before this unix timestamp in milliseconds. The window is half-open, so a run starting exactly on to_ms belongs to the next one.

job-run-summary

Field Type Required Description
running integer yes
started integer yes
failed integer yes
since_ms integer yes

job-run-summary-query

Field Type Required Description
since_ms string | integer yes

job-state

Type: "running" \| "unknown" \| "succeeded" \| "failed" \| "cancelled" \| "lost".

joint-state-put-request

Field Type Required Description
slug string | null yes The datapoint to read joint positions from, or null to choose none. It must name a whole-message sensor_msgs/msg/JointState datapoint of the published configuration; anything else is a validation_error naming the rule.

joint-state-put-response

Field Type Required Description
joint_state_slug string | null yes The stored mapping after the call, null when none is chosen. The same value assetListResponse.joint_state_slug carries.

live-session-response

Field Type Required Description
session_id string yes This viewer’s hold, and the only thing a release should be given. Two tabs of one logged-in user are two holds; releasing without an id lets go of both and leaves the other tab rendering a stream the robot has already stopped producing.
url string yes The LiveKit server to connect to, as a WebSocket URL.
room string yes The LiveKit room carrying this camera. Every viewer of one camera on one robot joins the same room, which is what makes the refcount hold meaningful.
token string yes The LiveKit access token to join room with. It is checked when the participant connects and not again afterwards — which is not the same as irrevocable: the cloud can still disconnect a participant after the fact, and does when membership, a role or a key changes.
expires_at string yes The deadline for joining, as an ISO 8601 timestamp — not a session backstop. A viewer who has already joined keeps receiving video past this moment, so cleanup belongs in an explicit release, never in a timer built on this value.

mail-outcome

Field Type Required Description
mail "sent" | "not_requested" | "not_configured" | "failed" yes What happened to the mail this call triggered. sent means the SMTP server accepted it, not that it was delivered; not_requested means none was attempted, because the caller asked for none or there was no link to carry; not_configured is an expected state and not a failure; failed is the one worth somebody’s attention.

mail-template-preview-request

Field Type Required Description
subject string yes The subject line, a Liquid template. Bounded because a subject is rendered into a header.
text string yes The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML.
html string | null no

mail-template-preview-response

Field Type Required Description
subject string yes The rendered subject line.
text string yes The rendered plain-text body.
html string | null yes The rendered HTML body, or null when the template is text-only.
Field Type Required Description
client_id string yes The MCP client this consent is for, as its dynamic registration was issued. It is the value the withdrawal routes take in their path, and it is the only stable handle on a client — the name beside it is not one.
client_name string | null yes What the client calls itself, or null once its registration is gone. Unverified — see client_name_verified.
client_name_verified false yes Always false. The client registered itself without authentication and named itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a true branch would be dead code that looked like a safeguard.
granted_at string yes When the consent was last given. A withdrawal followed by a fresh approval moves it, because the second approval is the agreement that stands — it is not a record of the first time anybody ever said yes.
Field Type Required Description
grants object[] yes Every standing consent this app user holds, newest first. Withdrawn ones are absent rather than listed as withdrawn; an app user who has connected no MCP client answers an empty array.
grants[].client_id string yes The MCP client this consent is for, as its dynamic registration was issued. It is the value the withdrawal routes take in their path, and it is the only stable handle on a client — the name beside it is not one.
grants[].client_name string | null yes What the client calls itself, or null once its registration is gone. Unverified — see client_name_verified.
grants[].client_name_verified false yes Always false. The client registered itself without authentication and named itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a true branch would be dead code that looked like a safeguard.
grants[].granted_at string yes When the consent was last given. A withdrawal followed by a fresh approval moves it, because the second approval is the agreement that stands — it is not a record of the first time anybody ever said yes.

mcp-robot-datasheet

Field Type Required Description
robot_id string yes
robot_name string yes
capabilities object yes
capabilities.action_history boolean yes
capabilities.assets boolean yes
exposures object[] yes
exposures[].slug string yes
exposures[].kind "datapoint" | "service" | "action" | "publisher" | "camera" yes
exposures[].description string | null yes
exposures[].unit string | null yes
exposures[].decimals integer | null yes
exposures[].input_schema unknown | null yes

mcp-role-preview-response

Field Type Required Description
role_id string yes
robots object[] yes
robots[].robot_id string yes
robots[].robot_name string yes
robots[].capabilities object yes
robots[].capabilities.action_history boolean yes
robots[].capabilities.assets boolean yes
robots[].exposures object[] yes
robots[].exposures[].slug string yes
robots[].exposures[].kind "datapoint" | "service" | "action" | "publisher" | "camera" yes
robots[].exposures[].description string | null yes
robots[].exposures[].unit string | null yes
robots[].exposures[].decimals integer | null yes
robots[].exposures[].input_schema unknown | null yes

missing-asset-query

The one optional parameter of the missing-asset placeholder; it names the reference in the refusal.

Field Type Required Description
name string no The unresolved reference, as the URDF spelled it, echoed into the 404 asset_missing message. Omitted, the message names unknown instead. It never changes the status.

oauth-authorize-query

The authorization request an MCP client sends, per RFC 6749 §4.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: client_id and redirect_uri are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client’s own callback as query parameters.

Field Type Required Description
response_type "code" yes Always code. RFC 6749 §4.1.2.1 names unsupported_response_type for any other value, but oauthErrorCode has no such member — this server issues no other grant from this endpoint — so an unsupported value comes back on the callback as invalid_request.
client_id string yes The OAuth client, self-registered or the one well-known central client — not the app identifier. Unknown, expired-dynamic and mismatched clients all collapse into the same 400 invalid_client, answered without a redirect.
redirect_uri string yes One of the client’s registered redirect URIs, compared exactly — string equality against the registered list, never a prefix or a host match. Both the shape (redirectUri) and the registration are checked, and a failure of either is a 400 invalid_request with no redirect.
code_challenge string yes The PKCE challenge; the verifier is presented at the token endpoint. Only non-emptiness is checked here — length and alphabet are not — since the verifier is what has to match.
code_challenge_method "S256" yes Only S256. plain is refused: a challenge equal to its verifier defends against nothing.
state string no Returned unchanged on the callback, and on the error redirect too, so a client can bind either answer to its own request.
resource string no RFC 8707 resource indicator: the API origin or the MCP endpoint the token is for. Checked against the resources this server issues tokens for on behalf of this client’s app; a mismatch is invalid_target on the callback.

oauth-token-request

What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other grant_type is unsupported_grant_type, refused before a lookup happens.

Field Type Required Description
grant_type "authorization_code" yes authorization_code: this request exchanges the code from the authorize redirect for an access token and a refresh token.
code string yes The authorization code from the redirect. It may be exchanged once; a second presentation is invalid_grant, the same answer a fabricated code gets.
redirect_uri string yes The same redirect URI the authorize request used. It is compared, not merely recorded.
client_id string yes The client making the exchange, as registered.
code_verifier string yes The PKCE verifier whose S256 hash was sent as the challenge at the authorize step. Between 43 and 128 unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1.
resource string no The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is invalid_target; omitted, the code’s own audience stands. It becomes the token’s aud, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app’s endpoint.
grant_type "refresh_token" yes refresh_token: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session.
refresh_token string yes The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is invalid_grant and stays unconsumed.
client_id string yes The client the refresh token was issued to, as registered. A refresh token is not transferable between clients.
resource string no The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is invalid_target and the refresh token is left untouched. The successor carries the same audience either way.

oauth-token-response

Field Type Required Description
access_token string yes The bearer token. It is the same token the client login mints — only the envelope differs, because an RFC-compliant client parses this one and knows nothing about Fleetless.
token_type "Bearer" yes Bearer. RFC 6749 §5.1 makes the value case-insensitive for a client reading it; this is the spelling this server emits.
expires_in integer yes How long the access token is valid, in seconds, per RFC 6749 §5.1. Not a timestamp, and not milliseconds.
refresh_token string no The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account’s sessions — a block, a password change, a withdrawn consent. The console’s own OAuth portal issues none.
scope string no The scopes the issued token actually carries, space-separated.

org-alerts-query

The query of GET /api/org/alerts. One required parameter with one accepted value.

Field Type Required Description
state "firing" yes Required, and the only accepted value — this endpoint lists the alerts that are firing now. Anything else, the parameter’s absence included, is 400 validation_error: a door with one answer must not advertise a dial.

org-firing-alerts-response

Field Type Required Description
alerts object[] yes
alerts[].id string yes
alerts[].robot_id string yes
alerts[].slug string yes
alerts[].name string yes
alerts[].enabled boolean yes
alerts[].severity "warning" | "error" yes
alerts[].condition object yes
alerts[].condition.kind "above" yes
alerts[].condition.threshold number yes
alerts[].condition.resolve_hysteresis number yes
alerts[].condition.kind "below" yes
alerts[].condition.threshold number yes
alerts[].condition.resolve_hysteresis number yes
alerts[].condition.kind "equals" yes
alerts[].condition.value number | string | boolean yes
alerts[].state "ok" | "firing" yes
alerts[].state_since string | null yes
alerts[].last_value unknown | null yes
alerts[].created_at string yes
alerts[].robot_name string yes

org-health-query

The optional robot filter of GET /api/org/health.

Field Type Required Description
robot_id string no Narrows the report to one robot. Omit it for every robot in the org. Malformed is 400 invalid_uuid and a robot of another org is 404 not_found — the same two answers an MCP caller gets, because the check lives in the shared service rather than on the route.

org-latency-query

Field Type Required Description
from_ms string | integer yes
to_ms string | integer yes
robot_id string no

org-latency-response

Field Type Required Description
series object[] yes
series[].robot_id string yes
series[].buckets object[] yes
series[].buckets[].bucket_at string yes
series[].buckets[].min_ms number | null yes
series[].buckets[].avg_ms number | null yes
series[].buckets[].max_ms number | null yes
series[].buckets[].samples integer yes
series[].buckets[].online_ms integer yes
from_ms integer yes
to_ms integer yes
truncated boolean yes
truncated_by "limit" | "bytes" | null yes

org-plan

Field Type Required Description
plan "basic" | "plus" | "pro" | "enterprise" yes The org’s current plan.
currency "eur" | "usd" yes The currency the org’s prices are shown and billed in.
period_ends_at string yes The end of the organization’s current billing period; while no payment period exists yet, the end of the current UTC calendar month. A pending downward plan change normally takes effect at this exact instant — except one chosen while the org was locked, which lands at once, and the platform’s own move off the beta, which lands at its switch date instead.
addons object yes The add-ons the org has bought. All zero on a plan without the addons feature.
addons.seats integer yes Extra developer seats, one each.
addons.robots integer yes Extra robots, one each.
addons.apps integer yes Extra apps, one each.
addons.app_user_packs integer yes Packs of five extra app users.
addons.live_video_packs integer yes Packs of 250 extra hours of app-user live video per month.
limits object yes The org’s effective limits: the plan’s, raised by its add-ons, or an operator’s override in their place. null means unlimited for a count, and 90 days for history_days and audit_days.
limits.seats integer | null yes Developers, owners included, plus pending team invitations.
limits.robots integer | null yes Robots in the org.
limits.apps integer | null yes Apps in the org.
limits.app_users integer | null yes App users across every app of the org, plus pending app-user invitations.
limits.live_video_ms_per_month integer | null yes Live video watched by app users in one UTC calendar month, in milliseconds, across the org. Console sessions do not count.
limits.asset_bytes_per_robot integer | null yes Asset storage per robot, in bytes (decimal: 1 GB = 1,000,000,000). The org stores up to robots × this value in total.
limits.history_days integer | null yes Days the org’s robot history is kept.
limits.audit_days integer | null yes Days the org’s audit log is kept.
features object yes What the org’s plan unlocks.
features.app_mcp boolean yes An app’s own MCP endpoint, for its app users.
features.two_factor boolean yes Two-factor sign-in for developers.
features.require_two_factor boolean yes An owner may require two-factor sign-in for every developer of the org.
features.app_oidc boolean yes An app may let its users sign in through an OpenID Connect identity provider.
features.hosted_logo boolean yes An app’s hosted pages show its logo and accent colour, not only its name.
features.audit_export boolean yes The audit log can be exported as CSV.
features.addons boolean yes Add-ons can be bought on top of the plan.
usage object yes What the org uses now, counted the way each limit counts it.
usage.seats integer yes Developers, owners included, plus pending team invitations.
usage.robots integer yes Robots in the org.
usage.apps integer yes Apps in the org.
usage.app_users integer yes App users across every app, plus pending app-user invitations.
usage.live_video_ms_this_month integer yes Live video watched by app users in the current UTC calendar month, in milliseconds. Console sessions do not count.
usage.asset_bytes integer yes Bytes the assets of every robot of the org occupy, together.
pending_change object | null yes A move to a lower plan that has not taken effect yet, or null.
pending_change.target_plan "basic" | "plus" | "pro" | "enterprise" yes The plan the org moves to.
pending_change.reason "downgrade" | "cancel" | "migration" | "lock" yes Why the change is pending: downgrade, cancel, migration or lock.
pending_change.effective_at string | null yes When the change takes effect. null only for a move off the beta whose date is not set yet.
pending_change.keep object | null yes What stays. null when the org already fits the target plan and nothing is deleted.
pending_change.keep.robots string[] yes The robots that stay, by id. Every other robot is deleted when the change takes effect.
pending_change.keep.apps string[] yes The apps that stay, by id. Every other app is deleted when the change takes effect.
pending_change.keep.app_users string[] yes The app users that stay, by id, across every app. Every other app user is deleted when the change takes effect.
pending_change.keep.developers string[] yes The developers that stay, by user id, owners never among them: every owner stays. Every other developer is removed from the org when the change takes effect.
pending_change.history_days_after integer yes Days of history and audit log the org keeps on the target plan.
pending_change.chosen_by string yes The user id of the owner who chose the change, or the nil UUID when Fleetless queued it.
pending_change.chosen_at string yes When the change was chosen.
lock object | null yes Why and since when the org is locked, or null when it is not.
lock.reason "payment" | "migration" yes payment: a payment is missing. migration: the org did not choose what stays when the beta ended.
lock.since string yes When the org was locked.
switch object | null yes Set only while the org is still on the beta; null for every other org.
switch.at string | null yes When the org moves from the beta onto its plan. null while the date is not set.
switch.needs_choice boolean yes Whether the org uses more than Basic allows, so an owner has to choose what stays before at.

org-quota-usage

Field Type Required Description
quotas object yes
quotas.max_robots integer yes
quotas.max_apps integer yes
quotas.max_end_users integer yes
quotas.max_retention_bytes integer yes
quotas.max_retention_writes_per_minute integer yes
quotas.max_realtime_connections integer yes
usage object yes
usage.max_robots integer no
usage.max_apps integer no
usage.max_end_users integer no
usage.max_retention_bytes integer no
usage.max_retention_writes_per_minute integer no
usage.max_realtime_connections integer no

org-usage-query

Field Type Required Description
from_day string yes
to_day string yes

org-usage-response

Field Type Required Description
rows object[] yes
rows[].app_id string | null yes
rows[].app_name string | null yes
rows[].metric "api_calls" | "live_session_ms" | "retention_bytes" | "asset_bytes" | "robot_online_ms" yes
rows[].day string yes
rows[].value integer yes
from_day string yes
to_day string yes

parameter-invalid-details

Field Type Required Description
violations object[] yes
violations[].field string yes
violations[].rule string yes
violations[].message string yes

parameter-violation

Field Type Required Description
field string yes
rule string yes
message string yes

password-change-request

Field Type Required Description
current_password string yes The password in use right now. It is required even though the session already proves identity: it is what makes a stolen session insufficient to take the account.
new_password string yes The replacement password. Every other session is revoked when it is accepted, while the session that made the change survives — logging somebody out of the tab they just used is indistinguishable from the change having failed.

patch-app-oidc-provider-request

Field Type Required Description
name string no A new display name for the provider.
issuer string no A new issuer URL. Changing it re-runs discovery; identities linked under the old one keep their (provider, subject) key.
client_id string no A new client id.
client_secret string no A replacement client secret. Absent means keep the stored one, so a routine edit need not put the secret back on the wire; it is never echoed back by any route.
scopes string[] no A replacement scope list. A replace, not a merge.
link_verified_emails boolean no Whether a federated login may join an existing app user by verified address.
enabled boolean no Turn the provider off or back on without deleting it or its linked identities.

patch-app-user-request

Field Type Required Description
display_name string | null no The user’s display name. Absent leaves it alone; an explicit null clears it.
role_id string no The role the user holds from now on. A role belonging to another app is 404 not_found, the same refusal a role that never existed gets; re-roling closes the user’s live subscriptions.
status "active" | "blocked" no Block the account or let it back in. pending_verification cannot be set here: it is reached only by self-registration and left only by spending the mailed verification token, so a developer setting it would strand the account in a state nothing re-mails them out of.

patch-auth-me-request

Field Type Required Description
display_name string | null yes

patch-fleetless-user-request

Field Type Required Description
display_name string | null no The member’s display name. Absent leaves it alone; an explicit null clears it.

patch-org-request

Field Type Required Description
name string no The organisation’s new display name. Absent leaves it alone.
require_two_factor boolean no Whether every member must have a second factor. Turning it on signs nobody out: each member without one sets it up at their next sign-in. Absent leaves it alone.

patch-org-response

Field Type Required Description
org object yes The organisation as it now stands, after the patch. The whole resource comes back, not only the changed fields.
org.id string yes The organisation. Every developer route is scoped to the caller’s org already, so a client rarely has to send this anywhere.
org.name string yes The organisation’s display name. Free text, changed through PATCH /api/org.
org.require_two_factor boolean yes Whether every member must have a second factor — a passkey or an authenticator app. A member without one sets it up at their next sign-in, before any session exists; nobody is signed out when it is switched on. It covers the console and the central MCP endpoint; server keys and robot bridges are not people and are not affected. Owners change it through PATCH /api/org.
org.created_at string yes When the organisation was created, as an ISO 8601 timestamp.

patch-robot-request

Field Type Required Description
name string yes

patch-robot-response

Field Type Required Description
robot object yes The robot as it now stands, after the patch. The whole resource comes back, not only the changed fields.
robot.id string yes The robot, and what every robot-scoped route takes as its :id.
robot.name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
robot.created_at string yes When the robot was created, as an ISO 8601 timestamp.

payment-method-change-request

Field Type Required Description
method "card" | "paypal" | "applepay" yes The new mandate’s method. Offer what billingView.payment_method_options lists: applepay may be refused with 400 validation_error { field: 'method', rule: 'applepay_needs_open_invoice' } unless an invoice is open.

pending-team-invite-list-response

Field Type Required Description
invitations object[] yes Pending invitations only. An accepted invitation is history, not something to revoke.
invitations[].id string yes The invitation, as revoked and re-issued by the team.
invitations[].email string yes The address the invitation was addressed to.
invitations[].tier "owner" | "developer" yes The tier the invitee would hold. Visible so an owner can spot an owner-tier invitation they did not authorise.
invitations[].expires_at string yes When the token stops working.

plan-change-request

Field Type Required Description
target_plan "basic" | "plus" | "pro" | "enterprise" yes The lower plan to move to; basic cancels.
keep object | null yes What stays. null when the org already fits the target plan, so nothing is deleted.
keep.robots string[] yes The robots that stay, by id. Every other robot is deleted when the change takes effect.
keep.apps string[] yes The apps that stay, by id. Every other app is deleted when the change takes effect.
keep.app_users string[] yes The app users that stay, by id, across every app. Every other app user is deleted when the change takes effect.
keep.developers string[] yes The developers that stay, by user id, owners never among them: every owner stays. Every other developer is removed from the org when the change takes effect.

protected-resource-metadata

Field Type Required Description
resource string yes The resource identifier this document describes, per RFC 9728. A token whose audience names something else is rejected here rather than merely noted.
authorization_servers string[] yes The authorization servers that may issue tokens for this resource. There is always at least one.
bearer_methods_supported "header"[] yes How a token may be presented: in the Authorization header only, never in a query parameter or a form field.
scopes_supported string[] no The scopes this resource understands, where it publishes a list.

publish-config-response

Field Type Required Description
version integer yes
published_at string yes

publish-request

Field Type Required Description
message record<string, unknown> yes The values to publish, keyed by the parameter names the publisher declares — the same flat form an invoke takes for params. They are checked against the declared bounds in the cloud before anything reaches the robot, and a slug another caller is still holding is refused with the remaining wait.

put-app-auth-look-request

Field Type Required Description
hosted_accent string | null yes The accent colour of the hosted pages, #rrggbb in lowercase, or null for the neutral shell’s own.

put-app-auth-mcp-request

Field Type Required Description
mcp_enabled boolean yes Whether this app serves an MCP endpoint at /mcp/<identifier>. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token.

put-app-auth-registration-request

Field Type Required Description
self_registration boolean yes Whether a stranger may create an account in this app. Off refuses POST /api/client/register with 403 registration_closed, and refuses an unknown identity at an OIDC callback with the same reasoning — one switch for one decision, whichever door the person arrives at.
allowed_domains string[] yes The email domains self-registration accepts, lowercase. An empty list means no domain restriction, not “nobody” — the switch above is what closes the door. An invitation always bypasses this, by password and through a provider alike.
allowed_origins string[] yes The origins the client auth API answers CORS for, and the only origins an OIDC redirect_uri may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match.

put-app-auth-sign-in-request

Field Type Required Description
sign_in_methods object yes Which sign-in methods the app offers: password, emailed code, or both — at least one. Identity providers stay on top of either. The default is password only.
sign_in_methods.password boolean yes Whether app users may sign in with a password. Off refuses POST /api/client/login with method_not_allowed, and registration and invitations then take no password.
sign_in_methods.email_code boolean yes Whether app users may sign in with a six-digit code mailed to them, valid ten minutes. A code needs no URL, so it works in local development and in an app with no web UI.
two_factor "off" | "optional" | "required" yes Whether the app asks for an authenticator code: off (the default), optional or required. A person with a confirmed authenticator is asked at every sign-in whatever the policy; a sign-in through an identity provider is never asked.

put-app-auth-urls-request

Field Type Required Description
app_url string | null yes The app’s own home page, linked as Open <app> when a hosted flow is done. null makes the hosted done page say You can close this tab.
invite_url string | null yes The page in the developer’s app that accepts an invitation, with {token} where the token goes. null means the hosted page in hosted_pages is used.
verify_url string | null yes The page that confirms a new address, with {token} where the token goes. null means the hosted page in hosted_pages is used.
reset_url string | null yes The page that takes a new password, with {token} where the token goes. null means the hosted page in hosted_pages is used.
mcp_login_url string | null yes The page an MCP authorization redirects to, with {interaction} where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it. null means the hosted MCP sign-in in hosted_pages is used.

put-app-mail-template-request

Field Type Required Description
subject string yes The subject line, a Liquid template. Bounded because a subject is rendered into a header.
text string yes The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML.
html string | null no

put-config-draft-request

Field Type Required Description
source string yes

put-robot-details-request

Field Type Required Description
details record<string, string | number | boolean | unknown[] | record<string, unknown>> yes

put-robot-details-response

Field Type Required Description
details record<string, string | number | boolean | unknown[] | record<string, unknown>> yes The stored robot_details document, which is the one that was just sent — this route replaces the document rather than merging into it. Keys are the developer’s own, lowercase and at most 64 characters; a value is a string of at most 4096 characters, a number, a boolean, an array or an object.

rate-limit-details

Field Type Required Description
retry_after_ms integer yes

recovery-codes-response

Field Type Required Description
recovery_codes string[] yes The ten new recovery codes, lowercase, shown once. Every earlier code is void.

refresh-request

Field Type Required Description
refresh_token string yes

release-live-query

Field Type Required Description
session_id string no The one hold to release, as the live session returned it. Absent releases all of this identity’s holds on this camera — the blunt form, still needed by a client that has lost its id or is going away, and the one that strands the identity’s other tabs.

rename-passkey-request

Field Type Required Description
name string yes The new name.

rename-slug-request

Field Type Required Description
from string yes
to string yes

rename-slug-response

Field Type Required Description
rewritten_grants integer yes
history_moved boolean yes
requires_publish true yes

resource-health-list-response

Field Type Required Description
resources object[] yes
resources[].robot_id string yes
resources[].kind "camera" yes
resources[].ref string yes
resources[].facet "source" | "publish" yes
resources[].state "ok" | "unreachable" | "auth_failed" | "unreadable_credential" | "credential_missing" | "stopped_by_config_change" | "publish_failed" | "unknown" yes
resources[].reason string | null yes
resources[].changed_at_ms integer yes

robot-delete-query

The one optional parameter of DELETE /api/robots/:id, and it is the difference between a refusal and a cascade. It accepts the exact string true, or its own absence, and refuses everything else.

Field Type Required Description
force "true" no Pass true to delete a robot that has a live session open; without it that is 409 robot_in_use. true and nothing else — any other value is 400 validation_error, reported against the field force with rule invalid_value, so a caller is never left believing they forced a deletion they did not. The deletion is a full cascade, which is why saying it is the whole decision.

robot-deletion-summary

Field Type Required Description
slug_count integer yes
sample_rows integer yes
bytes_freed integer yes
cameras string[] yes
asset_count integer yes
asset_bytes_freed integer yes What the robot’s store gives back: every distinct mesh or texture blob it holds, counted once, URDF excluded; a blob another robot also references stays in the object store but is still credited here, because each robot’s counter carries it.
job_run_count integer yes
had_live_session boolean yes
had_unpublished_draft boolean yes

robot-detail-response

Field Type Required Description
id string yes The robot, and what every robot-scoped route takes as its :id.
name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
created_at string yes When the robot was created, as an ISO 8601 timestamp.
bridge_state object yes
bridge_state.online boolean yes
bridge_state.latency_ms number | null yes
bridge_state.low_bandwidth boolean yes Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported.
exposes object yes
exposes.datapoints integer yes
exposes.actions integer yes
exposes.services integer yes
exposes.publishers integer yes
exposes.cameras integer yes
protocol_status "current" | "deprecated" | "refused" no Where this robot’s bridge stands against the protocol window: current, deprecated (still served, sunset date on the detail), or refused (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as current.
bridge_version string | null yes
protocol_version integer | null no The protocol version the bridge announced in its last accepted hello; null before the first. Absent from a cloud older than 0.21.0.
protocol object no The window verdict for protocol_version.
protocol.status "current" | "deprecated" | "refused" yes Same values as protocol_status.
protocol.sunset_at string | null yes ISO date the announced version stops being served; null when current or unknown.
last_hello_error object | null yes
last_hello_error.code string yes
last_hello_error.message string yes
last_hello_error.at string yes
config object yes
config.published_version integer | null yes
config.published_at string | null yes
config.draft_updated_at string | null yes
config.applied_version integer | null yes
config.applied_ok boolean | null yes
config.applied_errors object[] | null yes

robot-jobs-response

Field Type Required Description
jobs object[] yes At most one entry per slug — the current job there — ordered newest known first, and never null: a robot doing nothing answers an empty array. This is not a history endpoint. For an adopted job started_at is adoption time, so a job that has been running for an hour can sit above one started a minute ago.
jobs[].id string yes The job’s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.
jobs[].robot_id string yes The robot this job is running on.
jobs[].slug string yes The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.
jobs[].state "running" | "unknown" | "succeeded" | "failed" | "cancelled" | "lost" yes Where the job stands: running, unknown, succeeded, failed, cancelled or lost. unknown is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge’s next statement resolves it, error naming why the cloud lost sight of it. lost is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.
jobs[].origin "fleetless" | "external" yes Who started this job. fleetless for everything minted by the cloud; external for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to job_runs.
jobs[].started_at string yes When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is adoption time, not the real start — the cloud never minted it.
jobs[].updated_at string yes When this job last changed, as an ISO 8601 timestamp.
jobs[].seq integer yes A monotonic counter ascending in mint order, and the named tiebreaker for any listing that claims one — started_at alone is not a total order. Scoped per cloud process and per run: job state lives in memory, so this restarts with the registry it orders.
jobs[].result unknown | null yes What the call returned once it succeeded, shaped by the ROS action or service itself. null until then, and for a job that did not succeed.
jobs[].error object | null yes Why the job failed, or why the cloud does not know how it stands: a human message, a code where one exists, and details for the codes that carry a documented payload. Set on failed and lost, and on unknown — where code is bridge_disconnected or bridge_timeout, the cloud’s own reason for not knowing, cleared when the bridge reports the job running again.
jobs[].error.code string yes A machine-readable code for the failure, such as job_queue_full, where one exists for it.
jobs[].error.message string yes A human-readable sentence saying what went wrong.
jobs[].error.details unknown no The structured payload belonging to code, for the codes that document one — job_queue_full carries its limit and its queued count here. Absent for a failure with nothing structured to add, which is most of them.

robot-list-response

Field Type Required Description
robots object[] yes
robots[].id string yes The robot, and what every robot-scoped route takes as its :id.
robots[].name string yes The robot’s display name, at most 63 characters. Free text, changed through PATCH /api/robots/:id.
robots[].created_at string yes When the robot was created, as an ISO 8601 timestamp.
robots[].bridge_state object yes
robots[].bridge_state.online boolean yes
robots[].bridge_state.latency_ms number | null yes
robots[].bridge_state.low_bandwidth boolean yes Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported.
robots[].exposes object yes
robots[].exposes.datapoints integer yes
robots[].exposes.actions integer yes
robots[].exposes.services integer yes
robots[].exposes.publishers integer yes
robots[].exposes.cameras integer yes
robots[].protocol_status "current" | "deprecated" | "refused" no Where this robot’s bridge stands against the protocol window: current, deprecated (still served, sunset date on the detail), or refused (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as current.

robot-token-rotate-response

Field Type Required Description
token string yes The robot’s new bridge token. Returned exactly once; the previous token stops working at the bridge’s next hello.

role

Field Type Required Description
id string yes The role, and what an app user’s role_id and an app’s default_role_id refer to.
app_id string yes The app this role belongs to. Roles are never shared between apps, so a role id from another app reads as not_found.
name string yes The role’s name, shown wherever a user’s access is chosen. The two roles every app starts with are named observe and operate.
builtin boolean yes true for the two roles every app starts with. Their rights may be re-scoped exactly like a custom role’s, through PUT /api/apps/:id/roles/:roleId/permissions — the flag exists so the console can explain where they came from, not to protect them. Built-in roles can be renamed and deleted like any other; the flag only records that the cloud seeded them.

role-delete-query

Field Type Required Description
move_to string no Another role of the same app that takes over the deleted role’s app users, pending invitations and, when it applies, the app’s default. The role itself or a role of another app answers 400 validation_error.

role-list-response

Field Type Required Description
roles object[] yes The app’s roles, built-in and custom alike, ordered by created_at and then by name. The tie-break is not cosmetic — the two built-in roles are inserted in one statement and share a creation time to the microsecond, so never read a role by position.
roles[].id string yes The role, and what an app user’s role_id and an app’s default_role_id refer to.
roles[].app_id string yes The app this role belongs to. Roles are never shared between apps, so a role id from another app reads as not_found.
roles[].name string yes The role’s name, shown wherever a user’s access is chosen. The two roles every app starts with are named observe and operate.
roles[].builtin boolean yes true for the two roles every app starts with. Their rights may be re-scoped exactly like a custom role’s, through PUT /api/apps/:id/roles/:roleId/permissions — the flag exists so the console can explain where they came from, not to protect them. Built-in roles can be renamed and deleted like any other; the flag only records that the cloud seeded them.

role-permissions

Field Type Required Description
role_id string yes
grants object[] yes
grants[].robot_id string yes
grants[].slugs string[] yes
capabilities object yes
capabilities.action_history boolean yes
capabilities.presence boolean yes
capabilities.assets boolean yes

role-rename-request

Field Type Required Description
name string yes The new name, trimmed, 1 to 60 characters. Unique per app: another role of this app with the same name answers 409 role_name_taken. The role’s users keep it under its new name.

server-key-list-response

Field Type Required Description
server_keys object[] yes The app’s server keys as metadata, oldest first by created_at. The raw secret is not here and never will be: it exists once, in the response that created or rotated the key.
server_keys[].id string yes The key row, and what the rotate and delete routes address. It is not the key: this shape never carries the secret.
server_keys[].app_id string yes The app whose full rights this key carries. A key is never shared between apps.
server_keys[].name string yes A label the developer chose, so a key can be recognised before it is rotated or deleted.
server_keys[].created_at string yes When the key was minted, as an ISO 8601 timestamp. GET /api/apps/:id/server-keys orders by this field.
server_keys[].last_used_at string | null yes When this key last authenticated a request, or null if it never has — the cheapest way to spot a key nobody needs.

session-tokens

Field Type Required Description
access_token string yes The token to send as Authorization: Bearer <token> on every call. Short-lived: read expires_in rather than assuming a lifetime.
refresh_token string yes The token that buys the next access token. It rotates on every use, so a value presented twice is detectable theft and ends the whole family.
expires_in integer yes How long the access token stays valid, in seconds from now. Not a timestamp, and not milliseconds.

slug-usage-response

Field Type Required Description
grant_count integer yes
app_identifiers string[] yes
has_recorded_history boolean yes
alert_count integer yes

snapshot-meta-response

Field Type Required Description
slug string yes The camera this snapshot belongs to.
timestamp_ms integer | null yes When the stored frame was captured, as a unix timestamp in milliseconds. null means nothing has been captured yet, which is an answer rather than an error.
age_ms integer | null yes How old the stored frame is right now, in milliseconds; null when there is none. Snapshots are deliberately cheap and therefore deliberately old, and a cached frame served without its age is indistinguishable from a live one.
width integer | null yes Width of the stored frame in pixels, or null when nothing has been captured yet.
height integer | null yes Height of the stored frame in pixels, or null when nothing has been captured yet.
mime string | null yes The media type of the stored frame, such as image/jpeg, or null when nothing has been captured yet.

team-invite

Field Type Required Description
id string yes The invitation, as listed and revoked by the team.
email string yes The address the invitation was addressed to.
tier "owner" | "developer" yes The tier the invitee holds on acceptance, fixed when the invitation was created.
expires_at string yes When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does.
accept_url string yes The link to give the invitee, on the Fleetless auth portal. Bounded like idpIssuer: an unbounded URL on a shape that gets mailed, logged and rendered is a size nobody chose. Unlike an app invitation’s link this is never null — the portal is a page Fleetless does serve.
mail "sent" | "not_requested" | "not_configured" | "failed" yes What happened to the mail. not_configured is an expected state and not a failure; the link above is the primary path.

tier-change-request

Field Type Required Description
tier "owner" | "developer" yes

totp-confirm-request

Field Type Required Description
code string yes A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it.

totp-confirm-response

Field Type Required Description
recovery_codes string[] | null yes The ten recovery codes, shown once, when this is the account’s first second factor; null otherwise.

two-factor-setup-response

Field Type Required Description
secret string yes The shared secret, base32, for an authenticator app that cannot scan a QR code. Shown once; the cloud stores it encrypted.
otpauth_url string yes The same secret as an otpauth://totp/ URL, to render as a QR code. It carries the secret: never log it.

types-response

Field Type Required Description
types object[] yes
types[].name string yes
types[].kind "msg" yes
types[].fields unknown[] yes
types[].name string yes
types[].kind "srv" yes
types[].request unknown[] yes
types[].response unknown[] yes
types[].name string yes
types[].kind "action" yes
types[].goal unknown[] yes
types[].result unknown[] yes
types[].feedback unknown[] yes

update-app-request

Field Type Required Description
name string no
robot_ids string[] no
default_role_id string | null no

urdf-completeness

Field Type Required Description
present boolean yes Whether a URDF has been synced at all. Whether one could be synced is a different question, answered by urdf_available.
mesh_count integer yes How many distinct meshes the URDF references.
missing object[] yes The references nothing in the store answers, each with the element that asked for it. A bare count would send a developer hunting through the workspace by hand; the references are what they can act on.
missing[].uri string yes The reference, verbatim, that no stored asset answers — a package:// URI the workspace does not hold, or an absolute or bare relative path nothing will ever fetch.
missing[].element "mesh" | "texture" yes Which kind of reference it was: geometry the URDF names as a mesh, or a texture a surface paints with. Without it a client reports a missing texture as a missing mesh, contradicting mesh_count beside it.

vat-id-check-request

Field Type Required Description
country string yes The VAT ID’s country.
vat_id string yes The VAT ID as typed; normalized before the VIES lookup (normalizeVatId).

vat-id-check-response

Field Type Required Description
status "valid" | "unverified" | "invalid" yes VIES’s answer.
vat_id string yes The normalized VAT ID that was checked.
name string | null yes The registered holder, when VIES named one; null otherwise.

waitlist-request

Field Type Required Description
email string yes

webauthn-options-response

Field Type Required Description
options record<string, unknown> yes The PublicKeyCredentialCreationOptionsJSON or PublicKeyCredentialRequestOptionsJSON to pass to the browser. Its challenge is single-use and short-lived.