Skip to content

Cookie preferences

We use cookies for our own analytics and to see how our campaigns perform. We never sell your data. Necessary cookies keep the site working and cannot be switched off. See our Cookie Policy for details.

Security, load balancing and remembering your cookie choice.

Show us which pages people read and how they find the site, so we can improve it (Google Analytics).

Show us which of our ad campaigns bring visitors to the site (Google Ads).

© 2026 The ThingsBoard Authors
Try for free

ThingsBoard Cloud

Choose your data region

Your data stays in the region you choose, for residency and compliance. No credit card required.

Rather run it yourself? Install on your own servers

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.

Add the package to your project:

Terminal window
dart pub add thingsboard_paas_client
# or for Flutter projects
flutter pub add thingsboard_paas_client

This adds to pubspec.yaml:

dependencies:
thingsboard_paas_client: ^4.4.0

The 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;

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 — a TbStorage implementation that persists the JWT and refresh tokens between runs. InMemoryStorage is the default; LocalFileStorage is also bundled. Call tbClient.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.

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})');
}
}

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++;
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++;

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})');
}

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',
);

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();

Other subscription types follow the same pattern. Pass the matching command to TelemetrySubscriber and listen to the corresponding stream:

  • TimeseriesSubscriptionCmd and AttributesSubscriptionCmd — dataStream (used by createEntityAttributesSubscription above)
  • EntityDataCmd — entityDataStream, entity data query results with live telemetry updates
  • AlarmDataCmd — alarmDataStream
  • EntityCountCmd — entityCountStream
  • createNotificationsSubscription() and createNotificationCountSubscription() — notificationStream and notificationCountStream
  • reconnectStream fires after the WebSocket connection has been re-established

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.

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.