fleetlessfleetlessdocs
Reference/SDK/Client

SDK · Client

The client object, its options and configuration, the error type every refusal arrives as, the token store an app implements to persist a session, and the details shapes of the refusals a caller branches on.

Generated from the SDK’s own source. The SDK Reference is the narrative; this page is every member, in the order the SDK declares them.

createClient

ts
createClient(options: FleetlessClientOptions): FleetlessClient

Builds a client for one app. Pass tokenStore (or nothing — the default keeps the session in memory) for an app-user client that signs in with auth.login or a federated provider; pass serverKey for a server-side caller that never holds a user session; pass credentials when the embedder already holds the bearer and refreshes it itself. Passing more than one throws, because each is a different identity and a client acts as exactly one.

Nothing is fetched here: the realtime channel opens on the first subscription and closes on close() or auth.logout().

Parameter Type Required Description
options FleetlessClientOptions yes

Returns FleetlessClient.

References: FleetlessClientOptions, FleetlessClient

FleetlessClient

One app’s client, returned by createClient. Every API the SDK offers is a property on it, and all of them share this client’s identity, its single realtime channel and its token refresh.

Property Type Required Description
config FleetlessClientConfig yes The settled configuration, including the defaults createClient filled in.
auth AuthApi yes The whole client auth API: registration, verification, login, logout, password reset, invitations, the app’s federated sign-in providers, the MCP consent screen, and the app user’s own standing MCP grants.
datapoints DatapointsApi yes A topic’s latest value, a live subscription to it, and its recorded history.
actions ActionsApi yes Long-running work on the robot: invoke, cancel, and watch a job as it runs.
services ServicesApi yes Request/response calls to the robot that answer once and are done.
publishers PublishersApi yes One-way messages to a robot’s publisher, such as a velocity command.
cameras CamerasApi yes Camera snapshots, their age, and live video sessions.
jobs JobsApi yes Robot-wide job reads that do not fit under actions/services because they are not addressed by slug — see JobsApi.list.
assets AssetsApi yes URDF and mesh reads — list/get/urdf, plus the urdf-loader mesh callback.
robots RobotsApi yes Which robots this caller reaches, and what their role lets them do on each — the calls every screen starts from.

References: FleetlessClientConfig, AuthApi, DatapointsApi, ActionsApi, ServicesApi, PublishersApi, CamerasApi, JobsApi, AssetsApi, RobotsApi

close()

ts
close(): void

Closes the realtime channel and stops it from reconnecting. Safe with no subscription ever made, and safe to call twice. A Node script (the exact use case serverKey is for) that never calls this after subscribing will not exit on its own — an open WebSocket keeps the event loop alive. auth.logout() calls this automatically; call it yourself if the process should exit without logging out (e.g. a server-side shutdown).

Returns void.

FleetlessClientOptions

Everything createClient accepts. apiUrl and appIdentifier are required; the rest either select the kind of caller (tokenStore versus serverKey) or replace a global the SDK would otherwise reach for.

Property Type Required Description
apiUrl string yes Base URL of the Fleetless REST API, e.g. https://api.fleetless.dev.
appIdentifier string yes The app’s identifier (the slug shown in the console), sent on every login.
tokenStore TokenStore no Where refresh/access tokens live between calls. Defaults to in-memory — pass your own (localStorage, a cookie, a native keystore) to persist a session across reloads. The SDK never assumes a browser exists.
serverKey string no A server key (flk_...) for server-side callers with full app rights. Mutually exclusive with tokenStore-based login: a client constructed with a server key never calls auth.login/auth.logout.
credentials CredentialSource no A credential this client does not own. The caller answers both questions a bearer raises: what the token is right now (token()), and what to do when the server says it expired (handleExpired()). For an embedder that already holds a session and refreshes it itself — the Fleetless console is the case this exists for. Without it such a caller had to impersonate a TokenStore, and a cloud-side token_expired arriving while its own clock still read live posted one empty refresh whose validation_error had to be translated back into a session message. A source that answers handleExpired: false never builds a refresh request at all. Mutually exclusive with tokenStore and serverKey, which each own a credential of their own; a client acts as exactly one identity. auth.login/auth.logout refuse here for the same reason they refuse on a server key: there is no session for this client to start or end.
fetch (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> no Injectable for tests, or a non-global fetch implementation.
WebSocket (url: string | URL, protocols?: string | string[]) => WebSocket no Injectable for tests, or a non-global WebSocket implementation.
realtimeUrl string no Defaults to apiUrl with http(s) swapped for ws(s) and /realtime appended.

References: TokenStore, CredentialSource

FleetlessClientConfig

The settled configuration of a client, reachable as client.config. It is frozen and reflects the defaults createClient filled in, which is what makes it worth reading: realtimeUrl is usually derived rather than passed.

Property Type Required Description
apiUrl string yes The REST base URL this client calls, exactly as passed to createClient.
appIdentifier string yes The app this client acts as, exactly as passed to createClient.
realtimeUrl string yes The realtime WebSocket URL in use, derived from apiUrl unless one was passed.

FleetlessError

The one error type the SDK throws for a refused API call: a stable machine-readable code a caller can branch on (forbidden versus token_expired) plus a human message for logs and debugging. Never parse message — it is not part of the contract, only code is.

Catch it by shape rather than by class where you can (err.code), since a bundler that ends up with two copies of the SDK also ends up with two classes and instanceof then answers false for a genuine one.

Property Type Required Description
code FleetlessErrorCode yes What went wrong, as a stable string — the field to branch on.
details unknown no Structured detail the server sent with the refusal, if any: the violations behind parameter_invalid, the running job behind busy, retry_after_ms behind rate_limited. Parse it rather than assume its shape — parameterInvalidDetails is exported for exactly that.
status number no The HTTP status of the response that produced this error, if it came from one.

References: FleetlessErrorCode

new FleetlessError()

ts
new FleetlessError(code: FleetlessErrorCode, message: string, options?: FleetlessErrorOptions): FleetlessError

Builds an error. code is what a caller branches on and message is for a human; anything else the refusal carried goes in options.

Parameter Type Required Description
code FleetlessErrorCode yes
message string yes
options FleetlessErrorOptions no

Returns FleetlessError.

References: FleetlessErrorCode, FleetlessErrorOptions, FleetlessError

Inherited from Error: cause, name, message, stack.

FleetlessErrorOptions

The third argument of FleetlessError’s constructor: what a thrower can attach beyond the code and the message. Both fields are optional, and both are absent on the codes the SDK raises before any request is sent.

Property Type Required Description
details unknown no Field-level detail for validation errors, passed through verbatim.
status number no The HTTP status of the response that produced this error, if any.

FleetlessErrorCode

A stable code a caller can branch on: a server-defined code (open-ended — see ErrorCode’s own doc comment), one of SdkErrorCode’s client-side codes, or, since neither list is exhaustive, any other string. (string & {}) is the standard trick to keep autocomplete on the known values while still accepting an arbitrary one.

ts
type FleetlessErrorCode = ErrorCode | SdkErrorCode | string & {}

References: SdkErrorCode

SdkErrorCode

The union of SDK_ERROR_CODES — the SDK’s own client-side error vocabulary. A FleetlessError whose code is one of these was raised by this SDK rather than relayed from the server.

ts
type SdkErrorCode = typeof SDK_ERROR_CODES[number]

References: SDK_ERROR_CODES

SDK_ERROR_CODES

Codes the SDK produces itself rather than relaying from the server. Kept out of @fleetless/contracts’ ERROR_CODES deliberately — that list is the wire vocabulary, every entry something a server may actually send, and none of these are.

  • no_session / no_websocket: a client-side refusal before a request ever reaches the network (not logged in; no WebSocket implementation available). Named apart from the server’s own unauthorized so a caller can tell “the server refused me” from “the SDK refused before asking” by the code alone.
  • unparseable_error: the opposite direction — a real response did arrive, its body just was not shaped like the platform’s error format. Not a refusal at all, just “we do not know what the server said.”
  • command_timeout: a realtime command (invoke/cancel/publish) got no command_result within its timeout. The server may still answer later on the same socket — nobody knows — but the caller cannot be made to wait forever for that.
  • command_outcome_unknown: worse than a timeout, and told apart from it on purpose — the connection that carried the command was replaced by a new one (a reconnect) before any reply arrived. The server, if it answered at all, answered a socket that no longer exists, so the command may or may not have run. Never retried automatically — that could run an action twice — the caller recovers by reading the job (e.g. actions.subscribe), since state is observed by slug regardless of which connection asked for it.
  • unexpected_response: the server answered ok:true but left out something the command is defined to always return (e.g. no job on a successful invoke) — a contract violation the SDK noticed, not a refusal.
  • invalid_option: the caller passed an SDK-level argument or option that cannot mean what it looks like it means. Three cases so far: timeoutMs < patienceMs on invoke/call — the SDK would give up locally before the platform’s own patience runs out, and report command_timeout for a call the platform never actually refused; a non-string, non-null, non-omitted jobId on cancel — almost always a caller who upgraded past the older cancel(robotId, slug, options?) signature and is still passing an options object third; and a concurrency on assets.prepareUrdfScene that is not a positive integer, which would otherwise fetch nothing and return a scene that renders blank with no error to explain why. A fourth since 3.0.0: every auth method needing an app user’s own session, called on a client built with a serverKey — register, login, logout, the password and invitation calls, both OIDC calls, the two MCP decisions and the two grant calls. Those threw a bare Error before, which a caller could only catch by message. All of them are refused before any request is sent — as a rejection, since every one of those methods is async. A client-side mistake to fix, not something a server response could ever produce, which is why this code belongs here and not in @fleetless/contracts’ ERROR_CODES.
  • untrusted_absolute_url: the SDK refused to fetch an absolute URL whose origin does not match this client’s own apiUrl — thrown before the request is ever sent, so no Authorization header is ever built for it, let alone attached. The one caller that fetches an absolute URL at all is assets.createMeshLoader, following a URDF’s rewritten mesh URIs — and a URDF is ROS graph input, not first-party data, so an app rendering one must not silently trust wherever it points. assets.createMeshLoader’s onComplete surfaces this the same way it surfaces a network failure: (null, err).
  • no_urdf_synced: assets.prepareUrdfScene looked for a kind: 'urdf' row in assets.list() and found none. Thrown before any asset fetch, rather than left to surface as a confusing downstream failure from URDFLoader.parse(undefined) or similar — the caller’s fix is “sync a URDF first”, which this error can say directly.
  • aborted: assets.prepareUrdfScene() was given an AbortSignal and it fired — either already-aborted before the call started, or mid-flight while a fetch was in progress. Normalized to this one code regardless of which stage the abort landed in, rather than surfacing whatever shape the underlying fetch() rejects an aborted request with (a DOMException named AbortError in a browser, an Error named AbortError under Node’s fetch — two different shapes a caller would otherwise have to detect themselves to tell “I cancelled this” from “the network actually failed”). Every partial resource this call had already created (blob: URLs) is revoked before this throws — an aborted load must not leak what it fetched before the signal fired, the same guarantee a failed load already had.
  • state_mismatch: auth.completeOidcLogin() was called with a state that does not match the expectedState its own beginOidcLogin() returned for this attempt — or with no state at all (beginOidcLogin always sets one, so a callback carrying none does not look like a reply to a flow this client started), or with an empty expectedState, meaning nothing was persisted for this attempt at all. That last case is folded in rather than given its own code: two empty strings compare equal, so it has to be checked explicitly or the comparison defends nothing for exactly the callers most likely to hit it — but a caller branching on the code has the same next step either way, which is to start the sign-in again. Which of the two happened is in the message, because the developer’s remedies do differ (“check how your app persisted the value” versus “this response belongs to a sign-in you did not start”). Thrown before /api/client/oidc/exchange is ever called: RFC 6749 section 10.12’s whole point is that a caller must not complete an authorization response it did not itself request, so this check happens client-side, first, rather than being left to the server to catch — by which point a one-time code would already have been spent for a flow this client never started.
ts
const SDK_ERROR_CODES: readonly ["no_session", "no_websocket", "unparseable_error", "command_timeout", "command_outcome_unknown", "unexpected_response", "invalid_option", "untrusted_absolute_url", "state_mismatch", "no_urdf_synced", "aborted"]

TokenStore

Where the SDK keeps a session. A developer implements this to persist a login (localStorage, a cookie, a native keystore) — the SDK itself never assumes a browser, or any storage, exists.

load()

ts
load(): { access_token: string; refresh_token: string; expires_in: number } | Promise<{ access_token: string; refresh_token: string; expires_in: number } | null> | null

Returns the stored session, or null when nobody is logged in. May be async, so a store backed by a native keystore or an IndexedDB read works without a synchronous cache in front of it.

Returns { access_token: string; refresh_token: string; expires_in: number } | Promise<{ access_token: string; refresh_token: string; expires_in: number } | null> | null.

save()

ts
save(session: { access_token: string; refresh_token: string; expires_in: number } | null): void | Promise<void>

Writes the session, or clears it when passed null. Called after a login, after every silent refresh, and on logout — so an implementation that persists must expect to be called often, not once.

Parameter Type Required Description
session { access_token: string; refresh_token: string; expires_in: number } | null yes

Returns void | Promise<void>.

StoredSession

What is kept between calls to stay logged in: exactly the wire shape sessionTokens returns, no derived fields. Refresh is reactive (a call that meets an expired access token refreshes and retries) rather than proactive, so there is no expires_at to compute or drift out of sync.

ts
type StoredSession = SessionTokens

InMemoryTokenStore

The default store: works out of the box, forgets the session on reload.

new InMemoryTokenStore()

ts
new InMemoryTokenStore(): InMemoryTokenStore

Nothing is loaded from anywhere — a client built with it starts logged out.

Returns InMemoryTokenStore.

References: InMemoryTokenStore

load()

ts
load(): { access_token: string; refresh_token: string; expires_in: number } | null

Returns the session held in memory, or null if there is none.

Returns { access_token: string; refresh_token: string; expires_in: number } | null.

save()

ts
save(session: { access_token: string; refresh_token: string; expires_in: number } | null): void

Replaces the session held in memory; null clears it.

Parameter Type Required Description
session { access_token: string; refresh_token: string; expires_in: number } | null yes

Returns void.

CredentialSource

Supplies the Authorization header value for a request, and knows what to do when the server says the token has expired. HttpClient is agnostic to what is authenticating it — an end-user session with silent refresh, or a static server key — so both can share one request path.

token()

ts
token(): Promise<string | null>

The current raw bearer credential — a JWT or a server key (flk_...) — or null if not authenticated. Raw, not Bearer <token>: REST forms the Authorization header from it, and the realtime auth frame carries the very same value unprefixed, so there is exactly one place that knows what “the token” currently is.

Returns Promise<string | null>.

handleExpired()

ts
handleExpired(): Promise<boolean>

Called once when a request comes back token_expired. Three outcomes:

  • resolves true — a fresh credential is ready, retry the request;
  • resolves false — nothing to do (e.g. a server key, which cannot be refreshed at all), propagate the original token_expired;
  • throws a FleetlessError — refreshing itself failed for a specific, more useful reason (e.g. token_revoked, a reused refresh token); that error propagates instead of the original token_expired, so the caller learns what actually happened, not just that a retry was tried.

Returns Promise<boolean>.

RateLimitDetails

The rate-limit-details wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

BusyDetails

The busy-details wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

ParameterInvalidDetails

The parameter-invalid-details wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

ParameterViolation

The parameter-violation wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

parameterInvalidDetails

The details of a parameter_invalid refusal. Always at least one violation: a refusal that names none would leave the caller with nothing to fix. All violations are reported at once, not just the first — a caller fixing parameters one round-trip at a time is a caller who gives up.

ts
const parameterInvalidDetails: ZodType<ParameterInvalidDetails>

References: ParameterInvalidDetails

CancelRejectedDetails

The cancel-rejected-details wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

CancelReturnCode

One goal’s answer to a cancel: the ROS 2 CancelGoal return code its action server sent — 0 accepted (CANCEL_RETURN_CODES.none), 1 refused (rejected), 2 unknown goal (unknown_goal_id), 3 already ended (goal_terminated). The type of return_code in CancelRejectedDetails, where it is null when that goal’s server did not answer. The wire shape is contracts’ cancelReturnCode; the alias exists so the reference can describe it — a JSDoc on an export type { … } from statement does not survive bundling.

ts
type CancelReturnCode = CancelReturnCode$1

cancelRejectedDetails

The details of a cancel_rejected refusal: every goal the cancel reached, accepted ones included, each with the CancelGoal return code its action server answered — compare return_code against CANCEL_RETURN_CODES (none, rejected, unknown_goal_id, goal_terminated); it is null when that goal’s server did not answer within the bridge’s bound. Always at least one goal: the cloud refuses a cancel only because a goal’s server answered ERROR_REJECTED. Pinned here for the reason parameterInvalidDetails is: a caller parses it instead of reading the shape from prose.

ts
const cancelRejectedDetails: ZodType<CancelRejectedDetails>

References: CancelRejectedDetails

CANCEL_RETURN_CODES

The ROS 2 action_msgs/srv/CancelGoal return codes: 0 ERROR_NONE (the server accepted the cancel request), 1 ERROR_REJECTED (it refused), 2 ERROR_UNKNOWN_GOAL_ID, 3 ERROR_GOAL_TERMINATED (the goal had already ended). An accepted request is not an ended goal: whether the goal ends, and how, is what the action’s status reports afterwards, and reaches the cloud as the goal’s job_update.

ts
const CANCEL_RETURN_CODES: { none: 0; rejected: 1; unknown_goal_id: 2; goal_terminated: 3 }