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. |
mcp-consent-grant
| 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. |
mcp-consent-grant-list-response
| 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. |