Dart API Client
The Dart ThingsBoard API Client lets Dart and Flutter applications manage ThingsBoard entities, work with telemetry, and receive real-time updates over WebSocket. It is auto-generated from the ThingsBoard OpenAPI specification, so every REST endpoint and model of the platform is available as a typed API class. A handwritten runtime facade on top of the generated code handles JWT authentication with automatic token refresh, API key authentication, WebSocket telemetry subscriptions with auto-reconnect, and typed error handling.
The client is published as one package per target server. Each package version tracks the ThingsBoard server version it targets:
| Package | Target server | Version |
|---|---|---|
| thingsboard_ce_client | ThingsBoard Community Edition (releases before 4.4) | 4.3.0 |
| thingsboard_pe_client | Self-hosted ThingsBoard | 4.4.0 |
| thingsboard_paas_client | ThingsBoard Cloud | 4.4.0 |
The examples on this page use thingsboard_paas_client. All three packages share the same runtime facade and usage patterns; they differ only in the set of API classes and models generated from the REST API of the server they target.
Installation
Section titled “Installation”Add the package to your project:
dart pub add thingsboard_paas_client# or for Flutter projectsflutter pub add thingsboard_paas_clientThis adds to pubspec.yaml:
dependencies:thingsboard_paas_client: ^4.4.0The client requires Dart SDK 3.2 or later and targets ThingsBoard 4.4.0 or later. It runs on Flutter mobile (iOS, Android), Flutter Web, and plain Dart.
Import the package in Dart code:
import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;Getting started
Section titled “Getting started”Create a client instance, log in, inspect the authenticated user, and log out:
import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest( userName: 'tenant@thingsboard.org', password: 'tenant', )); print('isAuthenticated=${tbClient.isAuthenticated()}');
final authUser = tbClient.getAuthUser(); print('authUser: ${authUser?.userId} (${authUser?.authority})');
await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}ThingsboardClient accepts optional named parameters:
storage— aTbStorageimplementation that persists the JWT and refresh tokens between runs.InMemoryStorageis the default;LocalFileStorageis also bundled. CalltbClient.init()at startup to restore a stored session and refresh the token if it has expired.apiKey— authenticate with an API key instead of a user session (see below).onUserLoaded,onMfaAuth,onMfaForce,onError,onLoadStarted,onLoadFinished— callbacks for the authentication lifecycle, two-factor authentication prompts, request errors, and request activity.debugMode— logs every request and response to the console.
API key authentication
Section titled “API key authentication”You can authenticate with an API key instead of a username and password. Pass the key to the constructor; every request is then signed with it and no login() call is needed:
import 'package:thingsboard_paas_client/thingsboard_paas_client.dart';
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';const apiKey = 'tb_your_api_key';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint, apiKey: apiKey);try { final device = Device((b) => b ..name = 'myDevice' ..type = 'default'); final savedDevice = await tbClient.getDeviceControllerApi().saveDevice(device: device); print('savedDevice: ${savedDevice.data?.name} (${savedDevice.data?.id?.id})');} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}Fetch tenant devices
Section titled “Fetch tenant devices”Generated API classes are grouped by REST controller and obtained from the client with getXxxControllerApi() methods.
List endpoints take pageSize and page arguments and return the page wrapped in a Dio Response; the payload is in response.data.
Paginate through all devices belonging to the current tenant:
import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest(userName: 'tenant@thingsboard.org', password: 'tenant'));
final deviceApi = tbClient.getDeviceControllerApi(); var page = 0; PageDataDevice? devices; do { final response = await deviceApi.getTenantDevices(pageSize: 10, page: page); devices = response.data; for (final device in devices?.data ?? <Device>[]) { print('${device.name} (${device.type})'); } page++; } while (devices?.hasNext ?? false);
await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}Fetch tenant dashboards
Section titled “Fetch tenant dashboards”import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest(userName: 'tenant@thingsboard.org', password: 'tenant'));
final dashboardApi = tbClient.getDashboardControllerApi(); var page = 0; PageDataDashboardInfo? dashboards; do { final response = await dashboardApi.getTenantDashboards(pageSize: 10, page: page); dashboards = response.data; for (final dashboard in dashboards?.data ?? <DashboardInfo>[]) { print('${dashboard.title} (${dashboard.id?.id})'); } page++; } while (dashboards?.hasNext ?? false);
await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}Count and query entities
Section titled “Count and query entities”Models are immutable built_value classes created through a builder.
Polymorphic models such as entity filters and key filter predicates carry a type discriminator that you set explicitly.
Use the Entity Data Query API to count all devices and then only the active ones:
import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest(userName: 'tenant@thingsboard.org', password: 'tenant'));
final entityQueryApi = tbClient.getEntityQueryControllerApi();
// Entity filter that selects all devices final allDevicesQuery = EntityCountQuery((b) => b ..entityFilter = EntityTypeFilter((f) => f ..type = 'entityType' ..entityType = EntityType.DEVICE));
final totalDevices = await entityQueryApi.countEntitiesByQuery(entityCountQuery: allDevicesQuery); print('Total devices: ${totalDevices.data}');
// Same filter plus a key filter on the "active" attribute final activeDevicesQuery = allDevicesQuery.rebuild((b) => b ..keyFilters.add(KeyFilter((k) => k ..key.type = EntityKeyType.ATTRIBUTE ..key.key = 'active' ..valueType = EntityKeyValueType.BOOLEAN ..predicate = BooleanFilterPredicate((p) => p ..type = 'BOOLEAN' ..operation = BooleanOperation.EQUAL ..value.defaultValue = true))));
final activeDevices = await entityQueryApi.countEntitiesByQuery(entityCountQuery: activeDevicesQuery); print('Active devices: ${activeDevices.data}');
await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}Manage devices
Section titled “Manage devices”Create a device, save and read shared attributes through the Telemetry controller, then delete the device. Attribute and telemetry payloads are passed as JSON strings, exactly as the REST API expects them:
import 'dart:convert';import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest(userName: 'tenant@thingsboard.org', password: 'tenant'));
final deviceApi = tbClient.getDeviceControllerApi(); final telemetryApi = tbClient.getTelemetryControllerApi();
final device = Device((b) => b ..name = 'My test device' ..type = 'default' ..label = 'Created from Dart'); final savedDevice = (await deviceApi.saveDevice(device: device)).data!; final deviceId = savedDevice.id!.id; print('savedDevice: ${savedDevice.name} ($deviceId)');
final foundDevice = (await deviceApi.getDeviceInfoById(deviceId: deviceId)).data; print('foundDevice: ${foundDevice?.name}');
// Save shared attributes await telemetryApi.saveEntityAttributesV2( entityType: 'DEVICE', entityId: deviceId, scope: 'SHARED_SCOPE', body: jsonEncode({'targetTemperature': 22.4, 'targetHumidity': 57.8}), );
// Read shared attributes back final attributes = await telemetryApi.getAttributesByScope( entityType: 'DEVICE', entityId: deviceId, scope: 'SHARED_SCOPE', keys: 'targetTemperature,targetHumidity', ); for (final attribute in attributes.data ?? <AttributeData>[]) { print('${attribute.key} = ${attribute.value}'); }
// Delete device await deviceApi.deleteDevice(deviceId: deviceId);
await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}WebSocket subscriptions
Section titled “WebSocket subscriptions”tbClient.getTelemetryService() returns the WebSocket telemetry service. Wrap one or more subscription commands in a TelemetrySubscriber, listen to its stream, and call subscribe().
The subscriber reconnects automatically if the connection drops.
The example subscribes to the latest temperature and humidity telemetry of a device, then posts sample values through the REST API so the updates arrive over WebSocket.
The subscription helpers use the facade’s own EntityId and EntityType types, which share their names with generated models, so hide the generated ones and import the facade’s directly:
import 'dart:convert';import 'dart:math';import 'package:thingsboard_paas_client/thingsboard_paas_client.dart' hide LoginRequest, EntityId, EntityType;import 'package:thingsboard_paas_client/src/model/login_models.dart' show LoginRequest;import 'package:thingsboard_paas_client/src/model/id/entity_id.dart' show EntityId;import 'package:thingsboard_paas_client/src/model/id/entity_type_models.dart' show EntityType;
const thingsBoardApiEndpoint = 'https://thingsboard.cloud';
void main() async {final tbClient = ThingsboardClient(thingsBoardApiEndpoint);try { await tbClient.login(LoginRequest(userName: 'tenant@thingsboard.org', password: 'tenant'));
final deviceApi = tbClient.getDeviceControllerApi(); final telemetryApi = tbClient.getTelemetryControllerApi();
final device = Device((b) => b ..name = 'My test device' ..type = 'default'); final savedDevice = (await deviceApi.saveDevice(device: device)).data!; final deviceId = savedDevice.id!.id;
// Subscribe to the latest telemetry of the device final subscriber = TelemetrySubscriber.createEntityAttributesSubscription( telemetryService: tbClient.getTelemetryService(), entityId: EntityId.fromTypeAndUuid(EntityType.DEVICE, deviceId), attributeScope: 'LATEST_TELEMETRY', keys: ['temperature', 'humidity'], ); subscriber.dataStream.listen((update) { update.data.forEach((key, values) { for (final value in values) { print('$key = ${value.value} at ${value.ts}'); } }); }); subscriber.subscribe();
// Post sample telemetry final rng = Random(); for (var i = 0; i < 5; i++) { await Future.delayed(const Duration(seconds: 1)); await telemetryApi.saveEntityTelemetry( entityType: 'DEVICE', entityId: deviceId, scope: 'ANY', body: jsonEncode({ 'temperature': 10 + 20 * rng.nextDouble(), 'humidity': 30 + 40 * rng.nextDouble(), }), ); }
await Future.delayed(const Duration(seconds: 2)); subscriber.unsubscribe();
await deviceApi.deleteDevice(deviceId: deviceId); await tbClient.logout();} catch (e) { final error = toThingsboardError(e); print('ThingsBoard error: ${error.message} (${error.errorCode})');}}Other subscription types follow the same pattern. Pass the matching command to TelemetrySubscriber and listen to the corresponding stream:
TimeseriesSubscriptionCmdandAttributesSubscriptionCmd—dataStream(used bycreateEntityAttributesSubscriptionabove)EntityDataCmd—entityDataStream, entity data query results with live telemetry updatesAlarmDataCmd—alarmDataStreamEntityCountCmd—entityCountStreamcreateNotificationsSubscription()andcreateNotificationCountSubscription()—notificationStreamandnotificationCountStreamreconnectStreamfires after the WebSocket connection has been re-established
Error handling
Section titled “Error handling”Failed requests surface as a ThingsboardError with a message, an errorCode from the ThingsBoardErrorCode enum (for example authentication, jwtTokenExpired, permissionDenied, itemNotFound, tooManyRequests), and the HTTP status when available.
Facade methods such as login() throw it directly; generated API classes throw a Dio DioException that carries the ThingsboardError in its error field.
toThingsboardError() normalizes either form, so a single catch block covers both:
try {await tbClient.getDeviceControllerApi().getDeviceById(deviceId: 'missing-id');} catch (e) {final error = toThingsboardError(e);switch (error.errorCode) { case ThingsBoardErrorCode.itemNotFound: print('No such device'); case ThingsBoardErrorCode.permissionDenied: print('Not allowed: ${error.message}'); default: print('Request failed (${error.status}): ${error.message}');}}The onError constructor callback receives the same ThingsboardError for every failed request, which is convenient for global handling such as redirecting to the login screen when the session expires.
Migrating from the legacy client
Section titled “Migrating from the legacy client”The auto-generated client (thingsboard_ce_client 4.3.0, thingsboard_pe_client 4.4.0, thingsboard_paas_client 4.4.0) replaces the hand-written dart_thingsboard_client (thingsboard_client) and dart_thingsboard_pe_client (thingsboard_pe_client) packages, whose last release is 4.2.1.
The main differences:
| Legacy client (up to 4.2.1) | Auto-generated client |
|---|---|
thingsboard_client (CE), thingsboard_pe_client (PE) |
thingsboard_ce_client, thingsboard_pe_client, thingsboard_paas_client |
Hand-written services: tbClient.getDeviceService() |
One generated class per REST controller: tbClient.getDeviceControllerApi() |
getTenantDevices(PageLink(10)) |
getTenantDevices(pageSize: 10, page: 0) |
| Methods return the model directly | Methods return a Dio Response<T>; the model is in response.data |
Mutable models: Device('name', 'default') |
Immutable built_value models: Device((b) => b..name = 'name'..type = 'default') |
LoginRequest('user', 'password') |
LoginRequest(userName: 'user', password: 'password') from src/model/login_models.dart |
saveEntityAttributesV2(entityId, scope, {...}) |
saveEntityAttributesV2(entityType: 'DEVICE', entityId: id, scope: 'SHARED_SCOPE', body: jsonEncode({...})) |
| Requires ThingsBoard 3.6.3 or later | Requires ThingsBoard 4.4.0 or later |
The WebSocket layer (TelemetrySubscriber, subscription commands and streams) and the ThingsboardError type are carried over from the legacy client and work the same way.
More examples
Section titled “More examples”- Runnable example: paas/example
- Generated API reference for every controller and model: paas/doc
- Legacy client examples: dart_thingsboard_client/example
Was this helpful?