The Things Stack Industries Integration
The Things Stack Industries Integration connects ThingsBoard to The Things Stack Cloud or Enterprise, the LoRaWAN network server of The Things Industries. ThingsBoard connects to the MQTT server of your The Things Stack tenant as a client and subscribes to the uplinks of your application. An uplink data converter decodes each uplink into telemetry and attributes. For downlink, ThingsBoard publishes the encoded command to the same MQTT server, and The Things Stack delivers it to the device.
Use The Things Stack Industries Integration when:
- Your LoRaWAN devices are registered in an application on a private The Things Stack tenant, Cloud or Enterprise. For the public The Things Network, use the The Things Stack Community integration instead.
- You want ThingsBoard to create a device for every end device that sends data, named after its DevEUI.
- You need to send commands from ThingsBoard to those devices, for example when a shared attribute changes.
Architecture
Section titled “Architecture”- Receive data. The device sends an uplink through a LoRaWAN gateway to The Things Stack. The Things Stack publishes it as a JSON message to the topic
v3/{application}@{tenant}/devices/{device}/upof its MQTT server, where the integration has subscribed. - Unwrap the message. The uplink converter takes the device payload out of the message and extracts the network fields, such as the DevEUI, the frame port, and the signal quality, into the metadata.
- Decode the payload. The decoder turns the payload into telemetry and attributes. The converter adds the network fields you list and sets the device name, profile, and label. If Allow create devices or assets is on, ThingsBoard creates the device on its first uplink.
- Send a command (optional). The Rule Engine passes a message to the integration. The downlink converter encodes it, and ThingsBoard publishes it right away to
v3/{application}@{tenant}/devices/{device}/down/push. The Things Stack queues it and sends it to the device.
Prerequisites
Section titled “Prerequisites”- Access to ThingsBoard or ThingsBoard Cloud with integrations enabled for your tenant.
- Permission to create integrations and data converters, and to edit rule chains for downlink.
- An account on a The Things Stack Cloud or Enterprise tenant, with the right to create applications, end devices, and API keys.
- The MQTT server of the tenant must be reachable from ThingsBoard. ThingsBoard Cloud connects only to public addresses. For a server in a private network, use a Remote Integration.
Prepare The Things Stack
Section titled “Prepare The Things Stack”Create an application, get its MQTT credentials, and register an end device. This guide uses a test device whose uplinks you simulate in The Things Stack Console, so you can test the integration without hardware.
Create an Application
Section titled “Create an Application”- Sign in to the Console of your The Things Stack tenant.
- Go to Applications and click Add application.
- Enter an Application ID, for example
my-thingsboard. - Click Create application.
Get the MQTT Credentials
Section titled “Get the MQTT Credentials”The Things Stack runs an MQTT server for every tenant. ThingsBoard signs in to it with the application’s MQTT username and an API key as the password.
- In the application, go to Other integrations ⇾ MQTT.
- Copy the Public TLS address, for example
thingsboard-test.eu1.cloud.thethings.industries:8883. You need the host part for the integration. - Copy the Username. It has the form
{applicationId}@{tenantId}, for examplemy-thingsboard@thingsboard-test. - Click Generate new API key and copy the key. It is the Password.
Register an End Device
Section titled “Register an End Device”Register a test device with ABP (Activation by Personalization), so you can simulate uplinks right away without a join procedure. For real devices, use OTAA (Over-the-Air Activation): it creates new session keys on every join. See Adding devices in The Things Stack documentation.
- In the application, go to End devices and click Register end device.
- Under Input method, select Enter end device specifics manually.
- Select the Frequency plan, for example
Europe 863-870 MHz (SF9 for RX2 - recommended), the LoRaWAN version, for exampleLoRaWAN Specification 1.0.0, and the Regional Parameters version if the Console asks for it. - Click Show advanced activation, LoRaWAN class and cluster settings and select Activation by personalization (ABP). Keep the other settings.
- Under Provisioning information, click Generate next to DevEUI, Device address, AppSKey, and NwkSKey. The DevEUI becomes part of the device name in ThingsBoard.
- Enter an End device ID, for example
thermostat-a. ThingsBoard uses it to address downlinks. - Click Register end device.
Set Up The Things Stack Industries Integration
Section titled “Set Up The Things Stack Industries Integration”Create the uplink converter, then the integration with the MQTT credentials.
Create the Uplink Data Converter
Section titled “Create the Uplink Data Converter”The Things Stack Industries Integration uses the Typed uplink converter. ThingsBoard selects it for this integration type, and you cannot change it later. The converter unwraps The Things Stack message first and passes the decoder only the device payload, with the network fields in the metadata. The name, profile, and label of the device, and the network fields to store, are set in the form, not in the script. For details, see Typed uplink converter.
Write the decoder yourself, or take a ready-made one for your device model from the Converters Library.
-
Go to Integrations ⇾ Data converters and click + Add data converter ⇾ Create new converter.
-
Converter type — keep Uplink.
-
Integration type — select The Things Stack Industries.
-
Keep Create new.
-
Name — enter a name, for example
TTI Uplink Converter. -
Main decoding configuration:
- Entity type — keep Device.
- Device name — keep
Device $eui.$euiis replaced with the DevEUI from the metadata, for exampleDevice 70B3D57ED80054FE. You can use any metadata key from Decoder Inputs as$key. - The editor below the name opens in TBEL with the default decoder. Keep it: it reads a serial number, battery level, temperature, and saturation from an 8-byte payload. Use the TBEL / JS toggle to switch to JavaScript.
/*** Decodes the incoming payload and returns a structured object containing telemetry data and attributes.** @param {byte[]} input - The raw payload received as an array of bytes.* @returns {Object} output - The structured output with decoded telemetry and attributes.*/function decodePayload(input) {// Initialize the output object with empty attributes and telemetry for clarity.var result = { attributes: {}, telemetry: {}};// Detect input type and normalize to byte arrayvar bytes;if (metadata.payloadFormat == 'JSON') {// payloadFormat is 'JSON' when The Things Industries payload formatters are configured on the application or device level.// In that case, decoded_payload is a JSON object produced by your formatter.// This template assumes the formatter returns { "bytes": [...] } with the raw byte array.// If your formatter returns pre-decoded values in a different structure,// update this section to extract the values directly from 'parsed' instead.var parsed = decodeToJson(input);if (parsed.bytes == null) {throw new Error("Payload formatter output does not contain a 'bytes' field. " +"Update this decoder to match your formatter's output structure.");}bytes = parsed.bytes;} else {// payloadFormat is 'BINARY' when no payload formatter is configured — raw bytes from frm_payload are passed directly.bytes = input;}// Decode serial number (SN) from the first 4 bytes of the payload.// Press '?' icon in the top right corner to learn more about built in helper functions and capabilities.result.attributes.sn = parseBytesToInt(bytes, 0, 4);// Extract the timestamp from metadata (represented in milliseconds).var timestamp = metadata.ts; // ts is the timestamp parsed from the incoming message's time, or returns the current time if it cannot be parsed.// Combine the timestamp with decoded values and add it to the telemetry.result.telemetry = {ts: timestamp,values: {// Decode battery level from the 5th byte of the payload.battery: parseBytesToInt(bytes, 4, 1),// Decode temperature from the 6th and 7th bytes of the payload (divided by 100).temperature: parseBytesToInt(bytes, 5, 2) / 100.0,// Decode saturation from the 8th byte of the payload.saturation: parseBytesToInt(bytes, 7, 1)}};// Return the fully constructed output object.return result;}var result = decodePayload(payload);// Uncomment this code block to overwrite values set in the main configuration window. Useful if you extract device/asset/customer/group names from the payload;// result.type = 'DEVICE'; // Entity type allows you to choose type of created entity. Can be 'DEVICE' or 'ASSET'.// result.name = 'Temperature Sensor'; // Device or asset name (the value must be unique)// result.profile = 'IndustrialSensorProfile'; // Device or asset profile name.// result.customer = 'MyCustomer'; // If customer is not null - created entity will be assigned to customer with such name.// result.group = 'SensorsGroup'; // If group is not null - created entity will be added to the entity group with such name.// Return the final result object.return result;/*** Decodes the incoming payload and returns a structured object containing telemetry data and attributes.** @param {number[]} input - The raw payload received as an array of bytes.* @returns {Object} output - The structured output with decoded telemetry and attributes.*/function decodePayload(input) {// Initialize the output object with empty attributes and telemetry for clarity.var result = { attributes: {}, telemetry: {}};// Detect input type and normalize to byte array.var bytes;if (metadata.payloadFormat == 'JSON') {// payloadFormat is 'JSON' when The Things Industries payload formatters are configured on the application or device level.// In that case, decoded_payload is a JSON object produced by your formatter.// This template assumes the formatter returns { "bytes": [...] } with the raw byte array.// If your formatter returns pre-decoded values in a different structure,// update this section to extract the values directly from 'parsed' instead.var parsed = decodeToJson(input);if (parsed.bytes == null) {throw new Error("Payload formatter output does not contain a 'bytes' field. " +"Update this decoder to match your formatter's output structure.");}bytes = parsed.bytes;} else {// payloadFormat is 'BINARY' when no payload formatter is configured — raw bytes from frm_payload are passed directly.bytes = input;}// Decode serial number (SN) from the first 4 bytes of the payload.result.attributes.sn = parseBytesToInt(bytes, 0, 4);// Extract the timestamp from metadata (represented in milliseconds).var timestamp = metadata.ts; // ts is the timestamp parsed from the incoming message's time, or returns the current time if it cannot be parsed.// Initialize an object to store decoded key/value telemetry data.var values = {};// Decode battery level from the 5th byte of the payload.values.battery = parseBytesToInt(bytes, 4, 1);// Decode temperature from the 6th and 7th bytes of the payload (divided by 100).values.temperature = parseBytesToInt(bytes, 5, 2) / 100.0;// Decode saturation from the 8th byte of the payload.values.saturation = parseBytesToInt(bytes, 7, 1);// Combine the timestamp with values and add it to the telemetry.result.telemetry = {ts: timestamp,values: values};// Return the fully constructed output object.return result;}var result = decodePayload(payload);// Uncomment this code block to overwrite values set in the main configuration window. Useful if you extract device/asset/customer/group names from the payload;// result.type = 'DEVICE'; // Entity type allows you to choose type of created entity. Can be 'DEVICE' or 'ASSET'.// result.name = 'Temperature Sensor'; // Device or asset name (the value must be unique)// result.profile = 'IndustrialSensorProfile'; // Device or asset profile name.// result.customer = 'MyCustomer'; // If customer is not null - created entity will be assigned to customer with such name.// result.group = 'SensorsGroup'; // If group is not null - created entity will be added to the entity group with such name.// Return the final result object.return result;/*** Converts a byte array to a string and parses it as JSON.** @param {number[]} payload - The array of bytes.* @returns {Object} - The parsed JSON object.*/function decodeToJson(payload) {return JSON.parse(String.fromCharCode.apply(String, payload));}/*** Parse a slice of bytes from an array into an integer (big-endian).** @param {number[]} input - The array of bytes.* @param {number} offset - The starting index.* @param {number} length - The number of bytes to convert.* @returns {number} - The resulting integer.*/function parseBytesToInt(input, offset, length) {var result = 0;for (var i = offset; i < offset + length; i++) {result = (result << 8) | (input[i] & 0xFF);}return result;}The decoder reads these bytes:
Bytes Key Stored as Value 0–3 snattribute Serial number 4 batterytelemetry Battery level 5–6 temperaturetelemetry Raw value divided by 100 7 saturationtelemetry Saturation level To adapt:
- Byte layout — change the offset and length in each
parseBytesToInt()call to match your device payload. - Payload formatter — if the application or the device in The Things Stack has an uplink payload formatter, the decoder receives the formatter output as JSON, and
metadata.payloadFormatisJSON. Read the values fromparsedinstead, for examplevalues: { temperature: parsed.temperature }. - Device name or profile from the payload — set
result.nameorresult.profilein the script. They override the form.
-
Advanced decoding parameters — ThingsBoard fills them for The Things Stack Industries. Keep them for this guide:
- Device profile name —
$applicationId, the application ID, for examplemy-thingsboard. - Device label —
$deviceId, the end device ID, for examplethermostat-a. The downlink in this guide reads the end device ID from the label. - Customer name and Device group name — empty.
- Telemetry —
fCnt,data,decoded,latitude,longitude,altitude,rssi,snr, andchannel. - Attributes and Update only keys list —
eui,devAddr,fPort,bandwidth,spreadingFactor,codeRate,frequency,brandId,modelId,hardwareVersion,firmwareVersion,attributes, andtenantId.
The Telemetry and Attributes lists add these metadata keys to the decoder output, next to the keys that the decoder returns. A key is added only if the uplink has it and the decoder did not return a key with the same name. Keys in the Update only keys list are stored only when their value changes.
- Device profile name —
-
Click Test payload decoder and check the result (see Test the Decoder).
-
Click Add.
Decoder Inputs
| Variable | Type | Content |
|---|---|---|
payload |
byte array | The device payload. Without a payload formatter, the bytes of uplink_message.frm_payload, decoded from Base64. With a formatter, the JSON of uplink_message.decoded_payload. |
metadata |
object | The network fields that the converter extracts from the message, payloadFormat, ts, and the integration’s own keys: integrationName, topic, and the key–value pairs from the integration’s Metadata field. An integration key overrides a network field with the same name. |
The Things Stack sends each uplink as a JSON message. A simulated uplink from the test device looks like this:
{ "end_device_ids": { "device_id": "thermostat-a", "application_ids": { "application_id": "my-thingsboard" }, "dev_eui": "70B3D57ED80054FE", "dev_addr": "260B1A2C" }, "correlation_ids": ["as:up:01JB8Q4V0R5Y7Z2X3W4V5U6T7S"], "received_at": "2026-10-22T09:15:30.456Z", "uplink_message": { "f_port": 1, "frm_payload": "ALxhTl8JKVA=", "rx_metadata": [{ "gateway_ids": { "gateway_id": "test" }, "rssi": -42, "channel_rssi": -42, "snr": 4.2 }], "settings": { "data_rate": { "lora": { "bandwidth": 125000, "spreading_factor": 7 } }, "frequency": "868000000" }, "received_at": "2026-10-22T09:15:30.123Z" }, "simulated": true}Uplinks from real devices carry more fields, such as f_cnt, version_ids, and network_ids. The converter maps them to these metadata keys:
| Metadata key | Source in the message |
|---|---|
deviceId, applicationId |
end_device_ids.device_id, end_device_ids.application_ids.application_id |
eui, joinEui, devAddr |
end_device_ids.dev_eui, join_eui, dev_addr |
fPort, fCnt |
uplink_message.f_port, f_cnt |
data, decoded |
uplink_message.frm_payload (Base64 string), decoded_payload |
rssi, snr |
rssi and snr of the gateway in uplink_message.rx_metadata with the strongest signal |
rxMetadata |
uplink_message.rx_metadata, the full array |
bandwidth, spreadingFactor, codeRate, frequency, dataRateIndex |
uplink_message.settings |
latitude, longitude, altitude, source |
uplink_message.locations.user |
brandId, modelId, hardwareVersion, firmwareVersion, bandId |
uplink_message.version_ids |
netId, nsId, tenantId, clusterId, clusterAddress, tenantAddress |
uplink_message.network_ids |
receivedAt, uplinkMessageReceivedAt, time, timestamp |
received_at, uplink_message.received_at, uplink_message.settings.time, settings.timestamp |
receivedAtTs, uplinkMessageReceivedAtTs, timeTs |
The same times in milliseconds |
ts |
uplinkMessageReceivedAtTs, else receivedAtTs, else the current server time |
payloadFormat |
BINARY for frm_payload, JSON for decoded_payload |
correlationIds, sessionKeyId, consumedAirtime, attributes, simulated |
The fields with the same names |
A key is missing from the metadata when the message has no such field.
Test the Decoder
Click Test payload decoder below the editor. The dialog shows the Payload and the Metadata after unwrapping: a sample message for a new converter, or the last uplink once the converter has received data. To test with the message from Decoder Inputs, click the replay icon next to the payload, paste the message into Updated RAW payload, and apply it. ThingsBoard unwraps it again and fills in the payload and metadata.
Click Test. For the payload 00BC614E5F092950, the Decoder output panel shows:
{ "attributes": { "sn": 12345678 }, "telemetry": { "ts": 1792660530123, "values": { "battery": 95, "temperature": 23.45, "saturation": 80 } }}ts comes from uplink_message.received_at. When ThingsBoard saves the data, it also adds the keys from the Telemetry and Attributes lists that the uplink has: for the message above, data, rssi, and snr as telemetry, and eui, devAddr, fPort, bandwidth, spreadingFactor, and frequency as attributes. If the output is empty or shows an error, fix the decoder and test again.
Use a Converter from the Library
The Converters Library holds ready-made uplink converters for The Things Stack Industries, for devices from vendors such as AgroSense, Bosch, Decentlab, Elsys, Lansitec, and Milesight. See Converters library.
- Go to Integrations ⇾ Data converters and click + Add data converter ⇾ Create new converter.
- Keep Uplink, and select The Things Stack Industries as the Integration type.
- Switch the toggle from Create new to Library.
- Vendor — select the manufacturer of your device, for example
Milesight. - Model — select the device model, for example
AM102L. - Name — filled in for you, for example
Things Stack Industries Uplink Converter for Milesight AM102L. Change it if you like. - Check the decoder and the Advanced decoding parameters, then click Add.
You can also take a library converter in the integration wizard: click Library in the Uplink data converter step.
A library converter can be built in one of two ways, so check which one you got:
- Typed, like the converter above: the function is
payloadDecoder(payload, metadata), the form has Device name and the full Advanced decoding parameters, and the test button is Test payload decoder. The decoder gets the unwrapped device payload. - Generic: the function is
decoder(payload, metadata), Advanced decoding parameters has only the Update only keys list, and the test button is Test decoder function. The decoder gets the whole The Things Stack message and parses it itself. It also sets the device name and profile itself, for exampleAM102L thermostat-awith the profileAM102Lfor the Milesight AM102L. Most library converters for The Things Stack Industries are of this kind.
Create the Integration
Section titled “Create the Integration”- Go to Integrations ⇾ Integrations and click + Add integration.
- Basic settings:
- Integration type — select The Things Stack Industries.
- Name — keep the default
The Things Stack Industries integrationor enter your own. - Enable integration and Allow create devices or assets — keep both on, so the device is created on its first uplink.
- Click Next.
- Uplink data converter:
- Click Select existing and choose
TTI Uplink Converter. - Alternatively, click Create new to write a decoder here, or Library to take one from the Converters Library.
- Click Next.
- Click Select existing and choose
- Downlink data converter:
- Click Skip. Add a downlink converter later if you need to send commands.
- Connection:
- Host type — keep Region.
- Region — the part of the Public TLS address before
.cloud.thethings.industries, for examplethingsboard-test.eu1. ThingsBoard appends.cloud.thethings.industries. If your tenant has another address, select Custom as the Host type and enter the full host in Custom host. - Port — keep
8883. - Credentials — paste the MQTT Username, for example
my-thingsboard@thingsboard-test, and the API key as the Password. - Enable SSL — keep it on.
- Topic filters — keep
v3/+/devices/+/up. - Leave the other parameters at their defaults. See Connection Settings for details.
- Optionally, click Check connection. The wizard shows Connected when ThingsBoard can sign in to the MQTT server.
- Click Add.
The integration connects to the MQTT server right away and subscribes to the topic filters.
Connection Settings
Section titled “Connection Settings”| Parameter | Default | Description |
|---|---|---|
| Host type | Region | Region builds the host from the Region field. Custom takes the full host from Custom host, for an Enterprise deployment on your own domain. |
| Region | — | For Region. The part of the host before .cloud.thethings.industries, for example thingsboard-test.eu1. Required. Must be a public address on ThingsBoard Cloud. |
| Custom host | — | For Custom. The full host name of the MQTT server, without the port. Required. |
| Port | 8883 |
Port of the MQTT server, from 1 to 65535. 8883 is MQTT over TLS. Use 1883 only with Enable SSL off. |
| Username and Password | — | In Credentials. The MQTT username of the application, {applicationId}@{tenantId}, and an API key. Only this credentials type is offered, and both values are required. |
| Enable SSL | On | Connects over TLS and checks the server certificate against the trusted certificates of the ThingsBoard server. No client certificate is sent. |
| Topic filters | v3/+/devices/+/up, QoS At most once |
Topics to subscribe to. + matches one topic level, so the default receives the uplinks of every device the credentials can access. Subscribe only to .../up topics: the converter expects an uplink_message in every message. |
| Execute remotely | Off | Runs the integration as a separate process outside the ThingsBoard cluster, for an MQTT server in a private network. ThingsBoard generates an Integration key and Integration secret for it. See Remote Integration. |
| Protocol version | MQTT 3.1.1 |
In Advanced settings. MQTT 5 is not offered for this integration. |
| Max bytes in message | 32368 |
In Advanced settings. Maximum size of a received MQTT message, from 1 to 256000000 bytes. |
| Connection timeout (sec) | 10 |
In Advanced settings. How long ThingsBoard waits for the MQTT server to accept the connection, from 1 to 200 seconds. |
| Description | — | In Advanced settings. Free-text description. |
| Metadata | — | In Advanced settings. Key–value pairs added to the metadata of every message from this integration. |
| Downlink topic pattern | v3/{username}/devices/${devId}/down/push |
In Advanced settings. Read-only. ThingsBoard fills in the Username and replaces ${devId} with the devId value from the downlink converter result. |
ThingsBoard always connects with a clean session and a generated client ID.
Test Uplink
Section titled “Test Uplink”Simulate an uplink in The Things Stack Console, then confirm that ThingsBoard decoded it and created the device.
Send a Test Uplink
Section titled “Send a Test Uplink”- In the Console, go to End devices in your application and open
thermostat-a. - Open the Messaging tab and select Simulate uplink.
- FPort — keep
1. - Payload — enter
00BC614E5F092950. - Click Simulate uplink. The Console confirms that the uplink was sent.
The payload holds sn = 12345678, battery = 95, temperature = 23.45, and saturation = 80 in the byte layout of the default decoder.
Verify the Result
Section titled “Verify the Result”- Integration events — go to Integrations ⇾ Integrations, open the integration, and click the Events tab. The uplink appears as an event of type Uplink with status OK. Click
⋅⋅⋅in the Message column to see the topic and the message. - Converter events — open
TTI Uplink Converterin Integrations ⇾ Data converters and click the Events tab. In shows the full The Things Stack message, before unwrapping. Out shows the decoder result. Metadata showsintegrationNameandtopic. - Device — go to Devices & assets ⇾ Devices. The integration has created the device
Device 70B3D57ED80054FE, named after your DevEUI, with the profilemy-thingsboardand the labelthermostat-a. Open it and check:- Latest telemetry —
battery,temperature, andsaturationfrom the decoder, anddata,rssi, andsnrfrom the message. - Attributes —
snfrom the decoder, andeui,devAddr,fPort, and the radio settings in Client attributes.
- Latest telemetry —
Configure Downlink
Section titled “Configure Downlink”ThingsBoard publishes each downlink right away to the MQTT server, with QoS 1. Nothing is queued in ThingsBoard. The Things Stack adds the downlink to the queue of the device and sends it in the next receive window. A Class A device has one after each uplink.
The downlink converter turns a Rule Engine message into a The Things Stack downlink message and returns the end device ID for the topic. In this guide, a new shared attribute is sent to the device as JSON on the frame port of its last uplink. For the full encoder reference, see Downlink data converter.
Create and Assign the Downlink Converter
Section titled “Create and Assign the Downlink Converter”- Open the integration in Integrations ⇾ Integrations and click Toggle edit mode (pencil icon).
- In the Downlink data converter field, click Create new. The Downlink converter type and The Things Stack Industries integration type are preselected.
- Enter a name, for example
TTI Downlink Converter. - Main encoding configuration opens in TBEL with a generic encoder. It builds a JSON object from
temperatureUploadFrequencyandhumidityUploadFrequencyand sets an MQTTtopic, not thedownlinksarray anddevIdthat The Things Stack needs. Replace it with the encoder below. For JavaScript, use the TBEL / JS toggle and the JavaScript tab. - To record downlink events, click the debug button next to the name, select All messages, and click Apply.
- Click Add.
- Click Apply changes.
Encoder used in this guide. It sends the whole message as JSON, Base64-encoded in frm_payload, on the frame port from the fPort client attribute of the device:
/** Encoder **/
// End device ID. The rule chain adds it to the metadata as devId.var devId = metadata.devId;if (devId == null || devId == "") { raiseError("Missing required metadata field: devId");}
// Frame port of the last uplink. The rule chain adds the fPort client attribute as cs_fPort.if (metadata.cs_fPort == null) { raiseError("Missing required metadata field: cs_fPort");}var fPort = parseInt("" + metadata.cs_fPort);if (fPort < 1 || fPort > 223) { raiseError("Invalid LoRaWAN fPort. Expected a value from 1 to 223.");}
// The message as JSON, for example {"powerState":"on"}, encoded as Base64.var frmPayload = bytesToBase64(stringToBytes(JSON.stringify(msg)));
var data = { downlinks: [{ f_port: fPort, frm_payload: frmPayload, confirmed: false, priority: "NORMAL" }]};
return { contentType: "JSON", data: JSON.stringify(data), metadata: { // Replaces ${devId} in the downlink topic pattern. devId: devId }};/** Encoder **/
// End device ID. The rule chain adds it to the metadata as devId.var devId = metadata.devId;if (devId == null || String(devId).length === 0) { throw new Error("Missing required metadata field: devId");}
// Frame port of the last uplink. The rule chain adds the fPort client attribute as cs_fPort.var fPort = Number(metadata.cs_fPort);if (!Number.isInteger(fPort) || fPort < 1 || fPort > 223) { throw new Error("Invalid metadata.cs_fPort. Expected an integer from 1 to 223.");}
// The message as JSON, for example {"powerState":"on"}, encoded as Base64 from UTF-8.var json = JSON.stringify(msg);var binary = encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, function (match, hex) { return String.fromCharCode(parseInt(hex, 16));});var frmPayload = btoa(binary);
var data = { downlinks: [{ f_port: fPort, frm_payload: frmPayload, confirmed: false, priority: "NORMAL" }]};
return { contentType: "JSON", data: JSON.stringify(data), metadata: { // Replaces ${devId} in the downlink topic pattern. devId: String(devId) }};To check the encoder, click Test encoder function. Replace the Message with {"powerState": "on"}, add devId = thermostat-a and cs_fPort = 1 to Metadata, and click Test. The Output panel shows:
{ "contentType": "JSON", "data": "{\"downlinks\":[{\"f_port\":1,\"frm_payload\":\"eyJwb3dlclN0YXRlIjoib24ifQ==\",\"confirmed\":false,\"priority\":\"NORMAL\"}]}", "metadata": { "devId": "thermostat-a" }}The integration reads these fields from the encoder result:
| Field | Required | Description |
|---|---|---|
contentType |
Yes | JSON, TEXT, or BINARY. With JSON and TEXT, data is published as UTF-8 text. With BINARY, data must be Base64 and is published as the decoded bytes. |
data |
Yes | The message published to The Things Stack. It must hold a downlinks array, each entry with f_port and frm_payload (Base64), and optionally confirmed and priority. See MQTT Server in The Things Stack documentation. A result with empty data is skipped. |
metadata.devId |
Yes | The end device ID. ThingsBoard replaces ${devId} in the Downlink topic pattern with it, and replaces any other ${key} with the value of metadata.key. |
To adapt:
- Send only some keys — replace
JSON.stringify(msg)withJSON.stringify({ powerState: msg.powerState }). - Binary command — build the bytes yourself and pass them to
bytesToBase64()in TBEL, orbtoa()in JavaScript. - Confirmed downlink — set
confirmed: trueto ask the device for an acknowledgment. The Things Stack retries the downlink until the device acknowledges it, which can delay later downlinks. - Fixed port — replace the
cs_fPortlookup with a number, for examplevar fPort = 2;.
Configure the Root Rule Chain
Section titled “Configure the Root Rule Chain”The encoder needs the end device ID and the frame port. The uplink converter stores the end device ID as the device label and fPort as a client attribute. Add both to the metadata, then pass the message to the integration.
- Go to Data processing ⇾ Rule chains and open the Root Rule Chain.
- From the Enrichment section, drag an originator attributes node onto the canvas:
- Name — for example
Add fPort. - Client attributes —
fPort. The node adds it to the metadata ascs_fPort. - Add originator attributes to — keep Metadata.
- Click Add.
- Name — for example
- From the Enrichment section, drag an originator fields node onto the canvas:
- Name — for example
Add Device ID. - Under Originator fields mapping, set Source field to
Labeland Target key todevId. - Add mapped originator fields to — keep Metadata.
- Click Add.
- Name — for example
- From the Action section, drag an integration downlink node onto the canvas:
- Name — for example
Downlink to TTI. - Integration — select
The Things Stack Industries integration. - Click Add.
- Name — for example
- Connect the message type switch node to
Add fPortwith the Attributes Updated relation. - Connect
Add fPorttoAdd Device ID, andAdd Device IDtoDownlink to TTI, with the Success relation. - Click Apply changes.
Test Downlink
Section titled “Test Downlink”- Go to Devices & assets ⇾ Devices and open the device created in Test Uplink.
- On the Attributes tab, select Shared attributes and click +.
- Enter Key
powerState, keep the String type, enter Valueon, and click Add.
Verify the Downlink
Section titled “Verify the Downlink”- Converter events — open
TTI Downlink Converterin Integrations ⇾ Data converters and click the Events tab. In shows the message{"powerState": "on"}withcs_fPortanddevIdin its metadata. Out shows thedownlinksarray anddevId. - Integration events — open the integration’s Events tab. Two events of status OK appear: Downlink: ATTRIBUTES_UPDATED for the message from the Rule Engine, and Downlink for the published message, with the
topic, for examplev3/my-thingsboard@thingsboard-test/devices/thermostat-a/down/push, and thepayload. - The Things Stack — open the end device in the Console and click Live data. The downlink appears as a Receive downlink data message event. A real Class A device gets it after its next uplink.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| Check connection fails, or no events at all | The host is wrong: the Region does not match the Public TLS address of the tenant. | Enter the part of the Public TLS address before .cloud.thethings.industries, for example thingsboard-test.eu1, or select Custom and enter the full host. |
Failed to connect to MQTT broker at ... with a result code |
The Username or Password is wrong, or the API key was deleted or lacks the rights of the MQTT integration. | Copy the Username from Other integrations ⇾ MQTT again, in the form {applicationId}@{tenantId}. Generate a new API key and paste it as the Password. |
| Connection timeout | Port and Enable SSL do not match, or a firewall blocks the port. | Use 8883 with Enable SSL on. Allow outbound TCP to the port. |
Host must be in the public network., or Usage of local network host for MQTT broker connection is not allowed! |
The host is a private address, and the platform allows only public hosts. | Use the public address, or run the integration as a Remote Integration. |
| Connected, but no Uplink events | No uplinks arrive in the application, the topic filter was changed, or debug mode is off. | Check Live data of the end device in the Console. Keep the topic filter v3/+/devices/+/up. Turn debug mode on again. |
| Uplink events with status ERROR for messages that are not uplinks | A topic filter such as v3/+/devices/+/# also receives join, downlink, and other events, which have no uplink_message. |
Subscribe only to .../up topics. |
Converter error Payload formatter output does not contain a 'bytes' field. |
The application or device has a payload formatter, so the decoder gets its JSON output, not bytes. | Remove the formatter in The Things Stack, or read the values from parsed in the decoder. |
| Wrong values, or an error about the payload length | The byte offsets in the decoder do not match the device payload. | Compare the data value in the converter In with the offsets in parseBytesToInt(). |
| Device not created | Allow create devices or assets is off, or the device name resolves to an empty value. | Turn the option on. Check Device name in the converter and the metadata in the test dialog. |
Device named Device $eui |
The message has no end_device_ids.dev_eui, so $eui stays unresolved. |
Use another key in Device name, for example $deviceId. |
Attributes such as eui do not change on later uplinks |
Expected: keys in the Update only keys list are stored only when the value changes. | Remove a key from the list to store it with every uplink. |
| No downlink and no error | No downlink converter is assigned, or the rule chain does not route attribute updates. | Assign the converter and check the Attributes Updated relation. |
Downlink error Missing required metadata field: devId |
The device has no label, or the originator fields node is missing. | Keep Device label $deviceId in the uplink converter, or set the label of the device to the end device ID. Check the Add Device ID node. |
Downlink error Missing required metadata field: cs_fPort |
The device has no fPort client attribute yet, or the originator attributes node is missing. |
Send an uplink first. Check the Add fPort node. |
| Downlink event is OK, but nothing in Live data | The topic does not match the end device: the label is not the end device ID, or the username in the Downlink topic pattern is not the application’s. | Compare the topic in the integration event with the application ID, tenant ID, and end device ID in the Console. |
| Downlink in Live data, but the device does not react | A Class A device receives a downlink only after its next uplink. A confirmed downlink is retried until the device acknowledges it. | Wait for the next uplink. Use confirmed: false unless you need an acknowledgment. |
To read debug events, open the integration or converter, click Events, and select an event: In is the received data, Out is the converter result, and Error holds the error text.
See Also
Section titled “See Also”- Integrations overview — how integrations, converters, uplinks, and downlinks fit together.
- Uplink data converter — typed and generic converters, the Converters Library, and the decoder reference.
- Downlink data converter — encoder reference.
- The Things Stack Community integration — connect to the public The Things Network.
- integration downlink node — route Rule Engine messages to an integration.
- originator attributes node — add attributes of the message originator to the metadata.
- originator fields node — add fields of the message originator, such as the label, to the metadata.
- message type switch node — route messages by type, for example attribute updates.
- Remote Integration — run the integration outside the ThingsBoard cluster.
- TBEL — built-in functions and syntax for converter scripts.
- The Things Stack MQTT Server — topics, credentials, and the uplink and downlink message format.
- Adding devices — register end devices with OTAA or ABP.
Was this helpful?