Skip to content
Stand with Ukraine flag

IoT Device Contribution Guide

Welcome to ThingsBoard IoT Hub. This guide walks you through building a device package that end users can install on their ThingsBoard instance in a single click.

A device package is a ZIP file that bundles everything the installation wizard needs: configuration forms, setup instructions, ThingsBoard entity templates (device profiles, dashboards, rule chains), and images. When a user selects your device in IoT Hub, the wizard runs through your package step-by-step to provision the device, create a dashboard, and show the user how to connect the hardware.


Create a free Creator account on the ThingsBoard Creator Portal to start publishing your items.


When a user clicks Install on your device in IoT Hub, the installation wizard performs these actions in order:

  1. Displays instructions from your markdown files (prerequisites, wiring, firmware setup).
  2. Collects user input via forms you define (device name, Wi-Fi credentials, Device EUI, etc.).
  3. Creates ThingsBoard entities from your templates (device profile, device, dashboard, rule chain, integration, gateway).
  4. Resolves ${variable} placeholders in subsequent files using form values, created entity IDs, and platform transport settings.
  5. Shows post-install instructions with resolved variables (firmware code, verification steps, links to the created dashboard).

Your job as a creator is to describe all of this declaratively in device-info.json.

The fastest way to start is to grab a sample package, edit a few fields, and upload. Pick the one whose connectivity matches your device:

Then:

  1. Edit device-info.json — set your device’s name, vendor, description, hardwareType, connectivity, and useCases.
  2. Replace the entity templates (device-profile.json, device.json, dashboard.json) with exports from your own ThingsBoard instance. Keep the ${variable} placeholders.
  3. Rewrite prerequisites.md and post-install.md for your hardware.
  4. Swap in your device photo under images/.
  5. Re-zip the contents (not the folder itself) and upload via IoT Hub → + Add new item.

See Package structure if your device supports multiple connectivity options.

The sections below are a complete reference for every part of a device package.

Use this layout when your device supports only one way of connecting. All files live at the ZIP root.

my-device.zip
├── device-info.json (required — metadata + install steps)
├── overview.md (optional — long description for the device detail page)
├── images/
│ ├── device-photo.jpg
│ └── wiring-diagram.png
├── prerequisites.md (shown before install)
├── form.json (user input fields)
├── device-profile.json (ThingsBoard entity template)
├── device.json (ThingsBoard entity template)
├── dashboard.json (ThingsBoard entity template)
└── post-install.md (shown after install)

Use this layout when your device supports several ways of connecting (e.g. a LoRaWAN sensor that works with ChirpStack, TTN, and LORIOT). Shared files go in common/; method-specific files go in per-method subfolders.

my-device.zip
├── device-info.json
├── overview.md
├── images/
│ └── device-photo.jpg
├── common/
│ ├── form.json (shared fields: device name, Device EUI)
│ ├── device-profile.json
│ ├── device.json
│ └── dashboard.json
├── chirpstack/
│ ├── prerequisites.md
│ ├── form.json (ChirpStack-specific: server URL, API token)
│ ├── converter.json
│ ├── integration.json
│ └── post-install.md
├── ttn/
│ ├── prerequisites.md
│ ├── form.json (TTN-specific: region, app ID, API key)
│ ├── converter.json
│ ├── integration.json
│ └── post-install.md
└── loriot/
├── prerequisites.md
├── form.json
├── converter.json
├── integration.json
└── post-install.md

When using Layout B, each install method has a conventional subfolder name. The folder name is referenced from the file and template paths in installStepsyou must match it exactly.

The folder name is purely a path reference — what matters is that the file and template values in installSteps correctly point to existing files in the ZIP. The conventions below avoid collisions when a single device package supports multiple install methods that share a protocol name (e.g. both DIRECT_MQTT and INTEGRATION_MQTT).

Direct transports

Install MethodRecommended Folder
DIRECT_HTTPdirect_http/
DIRECT_MQTTdirect_mqtt/
DIRECT_COAPdirect_coap/
DIRECT_LWM2Mdirect_lwm2m/
DIRECT_SNMPdirect_snmp/

ThingsBoard IoT Gateway connectors

Install MethodRecommended Folder
GATEWAY_MQTTgateway_mqtt/
GATEWAY_MODBUSgateway_modbus/
GATEWAY_OPCUAgateway_opcua/
GATEWAY_BACNETgateway_bacnet/
GATEWAY_BLEgateway_ble/
GATEWAY_CANgateway_can/
GATEWAY_FTPgateway_ftp/
GATEWAY_KNXgateway_knx/
GATEWAY_OCPPgateway_ocpp/
GATEWAY_ODBCgateway_odbc/
GATEWAY_REQUESTgateway_request/
GATEWAY_RESTgateway_rest/
GATEWAY_SNMPgateway_snmp/
GATEWAY_SOCKETgateway_socket/
GATEWAY_XMPPgateway_xmpp/

ChirpStack (CE)

Install MethodRecommended Folder
CHIRPSTACKchirpstack/

ThingsBoard PE integrations

Install MethodRecommended Folder
INTEGRATION_APACHE_PULSARapache_pulsar/
INTEGRATION_AWS_IOTaws_iot/
INTEGRATION_AWS_KINESISaws_kinesis/
INTEGRATION_AWS_SQSaws_sqs/
INTEGRATION_AZURE_EVENT_HUBazure_event_hub/
INTEGRATION_AZURE_IOT_HUBazure_iot_hub/
INTEGRATION_AZURE_SERVICE_BUSazure_service_bus/
INTEGRATION_CHIRPSTACKchirpstack/
INTEGRATION_COAPintegration_coap/
INTEGRATION_CUSTOMcustom/
INTEGRATION_HTTPintegration_http/
INTEGRATION_IOT_CREATORSiot_creators/
INTEGRATION_KAFKAkafka/
INTEGRATION_KPN_THINGSkpn_things/
INTEGRATION_LORIOTloriot/
INTEGRATION_MQTTintegration_mqtt/
INTEGRATION_OPC_UAintegration_opc_ua/
INTEGRATION_PARTICLEparticle/
INTEGRATION_PUB_SUBpub_sub/
INTEGRATION_RABBITMQrabbitmq/
INTEGRATION_REMOTEremote/
INTEGRATION_SIGFOXsigfox/
INTEGRATION_TCPintegration_tcp/
INTEGRATION_THINGPARKthingpark/
INTEGRATION_THINGPARK_ENTERPRISEthingpark_enterprise/
INTEGRATION_TTItti/
INTEGRATION_TTNttn/
INTEGRATION_TUYAtuya/
INTEGRATION_UDPintegration_udp/

Any file referenced from multiple install methods should live in common/ to avoid duplication:

"installSteps": {
"CHIRPSTACK": [
{"type": "SHOW_FORM", "name": "Configuration", "file": "common/form.json"},
{"type": "DEVICE_PROFILE", "name": "My Sensor", "template": "common/device-profile.json"},
...
],
"INTEGRATION_TTN": [
{"type": "SHOW_FORM", "name": "Configuration", "file": "common/form.json"},
{"type": "DEVICE_PROFILE", "name": "My Sensor", "template": "common/device-profile.json"},
...
]
}

The device-info.json file at the ZIP root is the single source of truth for your package. The wizard reads it first and uses it to drive everything else.

{
"name": "My Temperature Sensor",
"description": "Wi-Fi temperature sensor with MQTT connectivity.",
"vendor": "Acme IoT",
"hardwareType": "Sensor",
"connectivity": ["Wi-Fi", "MQTT"],
"useCases": ["Environment Monitoring", "Smart Office"],
"image": "images/device-photo.jpg",
"installMethods": ["DIRECT_MQTT"],
"installSteps": {
"DIRECT_MQTT": [
{"type": "SHOW_INSTRUCTION", "name": "Prerequisites", "file": "prerequisites.md"},
{"type": "SHOW_FORM", "name": "Configuration", "file": "form.json"},
{"type": "DEVICE_PROFILE", "name": "Acme Temperature Sensor", "template": "device-profile.json"},
{"type": "DEVICE", "name": "${deviceName}", "template": "device.json"},
{"type": "DASHBOARD", "name": "Temperature Sensor Monitor", "template": "dashboard.json"},
{"type": "SHOW_INSTRUCTION", "name": "Setup Complete", "file": "post-install.md"}
]
}
}
FieldTypeDescription
namestringDevice model name as shown on the marketplace card
descriptionstringOne-sentence description (max 512 chars) shown on the card
vendorstringManufacturer name
hardwareTypestringOne value from Hardware types
installMethodsstring[]One or more values from Install methods
installStepsobjectOrdered steps per install method — keys must match installMethods
FieldTypeDescription
connectivitystring[]Tags from Connectivity tags — used for browse filtering
useCasesstring[]Tags from Use cases — used for browse filtering
imagestringPath to the device photo in the ZIP (e.g. images/device.png)
productURLstringManufacturer’s product page URL — backs ${product.button} (see Documentation links)
datasheetURLstringDatasheet URL — backs ${datasheet.button} (see Documentation links)
  • Every key in installSteps must exactly match a value in installMethods.
  • All file and template paths are relative to the ZIP root, not to device-info.json.
  • The name on DEVICE_PROFILE and DASHBOARD steps must be specific to your device (e.g. "ESP32 PICO KIT", not "Default"; "ESP32 PICO KIT Monitor", not "Check and control device data dashboard"). Users see these names in their ThingsBoard instance.
  • The DEVICE step name should use ${deviceName} so the device is named from the form input.

Pick the one that best describes your device. Allowed values: Actuator, AI Accelerator, Analyzer, Camera, Controller, Data Logger, Development Board, Display, Gateway, Meter, PLC, Relay, Sensor, Single Board Computer, Tracker.

If your device doesn’t fit any of these, pick the closest match and contact support to request a new type.

List every physical interface and protocol your device supports. The more accurate your tags, the easier users can find your device when browsing.

Wireless: Wi-Fi, Bluetooth, LoRaWAN, Zigbee, Z-Wave, Thread, NB-IoT, LTE-M, Sigfox, NFC, 2G, 3G, 4G, 5G, 6LoWPAN, DigiMesh, UWB.

Wired: Ethernet, RS485, RS232, USB, UART, SPI, I2C, CAN, 1-Wire, 4-20mA, 0-10V, M-Bus, IO-Link, IrDA, Power Line Communication, SDI-12.

Protocols: MQTT, HTTP, HTTPS, CoAP, REST, SNMP, Modbus RTU, Modbus TCP, OPC-UA, BACnet, KNX, WebSocket, TCP/IP, UDP, CANopen, DNP3, EtherCAT, Foundation Fieldbus, HART, IEC 61850, PROFINET, Wireless M-Bus.

Tag your device with all applicable IoT use cases. These are shared across the whole marketplace (widgets, dashboards, devices), so users browsing by use case will find your device alongside matching content.

Allowed values: Air Quality Monitoring, Asset Tracking, Cold Chain, Drones, Environment Monitoring, Fleet Tracking, Health Care, Industrial Automation, Predictive Maintenance, Robotics, SCADA, Smart Building, Smart City, Smart Energy, Smart Farming, Smart Home, Smart Metering, Smart Office, Smart Retail, Solar Monitoring, Tank Level Monitoring, Waste Management.

Choose one or more methods that describe how the device connects to ThingsBoard. Each method you list must have a matching entry in installSteps.

Direct transports — device opens a transport connection straight to the platform.

ValueDescriptionCEPE
DIRECT_HTTPDevice connects directly via HTTPyesyes
DIRECT_MQTTDevice connects directly via MQTTyesyes
DIRECT_COAPDevice connects directly via CoAPyesyes
DIRECT_LWM2MDevice connects directly via LwM2Myesyes
DIRECT_SNMPDevice connects directly via SNMPyesyes

ThingsBoard IoT Gateway connectors — device talks to a ThingsBoard IoT Gateway, which forwards to the platform. One value per Gateway connector.

ValueDescriptionCEPE
GATEWAY_MQTTGateway MQTT connectoryesyes
GATEWAY_MODBUSGateway Modbus connectoryesyes
GATEWAY_OPCUAGateway OPC-UA connectoryesyes
GATEWAY_BACNETGateway BACnet connectoryesyes
GATEWAY_BLEGateway BLE connectoryesyes
GATEWAY_CANGateway CAN connectoryesyes
GATEWAY_FTPGateway FTP connectoryesyes
GATEWAY_KNXGateway KNX connectoryesyes
GATEWAY_OCPPGateway OCPP connectoryesyes
GATEWAY_ODBCGateway ODBC connectoryesyes
GATEWAY_REQUESTGateway Request connectoryesyes
GATEWAY_RESTGateway REST connectoryesyes
GATEWAY_SNMPGateway SNMP connectoryesyes
GATEWAY_SOCKETGateway Socket connectoryesyes
GATEWAY_XMPPGateway XMPP connectoryesyes

ChirpStack (CE-compatible)

ValueDescriptionCEPE
CHIRPSTACKChirpStack LoRaWAN integration (CE-compatible)yesyes

ThingsBoard PE integrations — uses the platform’s Integrations subsystem. PE only

ValueDescriptionCEPE
INTEGRATION_APACHE_PULSARApache Pulsar integrationnoyes
INTEGRATION_AWS_IOTAWS IoT integrationnoyes
INTEGRATION_AWS_KINESISAWS Kinesis integrationnoyes
INTEGRATION_AWS_SQSAWS SQS integrationnoyes
INTEGRATION_AZURE_EVENT_HUBAzure Event Hub integrationnoyes
INTEGRATION_AZURE_IOT_HUBAzure IoT Hub integrationnoyes
INTEGRATION_AZURE_SERVICE_BUSAzure Service Bus integrationnoyes
INTEGRATION_CHIRPSTACKChirpStack integration (PE)noyes
INTEGRATION_COAPCoAP integrationnoyes
INTEGRATION_CUSTOMCustom integrationnoyes
INTEGRATION_HTTPHTTP integrationnoyes
INTEGRATION_IOT_CREATORSIoT Creators (Deutsche Telekom) integrationnoyes
INTEGRATION_KAFKAApache Kafka integrationnoyes
INTEGRATION_KPN_THINGSKPN Things integrationnoyes
INTEGRATION_LORIOTLORIOT LoRaWAN network servernoyes
INTEGRATION_MQTTMQTT integrationnoyes
INTEGRATION_OPC_UAOPC-UA integrationnoyes
INTEGRATION_PARTICLEParticle integrationnoyes
INTEGRATION_PUB_SUBGoogle Pub/Sub integrationnoyes
INTEGRATION_RABBITMQRabbitMQ integrationnoyes
INTEGRATION_REMOTERemote integrationnoyes
INTEGRATION_SIGFOXSigfox integrationnoyes
INTEGRATION_TCPTCP integrationnoyes
INTEGRATION_THINGPARKThingPark Wireless integrationnoyes
INTEGRATION_THINGPARK_ENTERPRISEThingPark Enterprise integrationnoyes
INTEGRATION_TTIThe Things Industries integrationnoyes
INTEGRATION_TTNThe Things Stack / The Things Networknoyes
INTEGRATION_TUYATuya integrationnoyes
INTEGRATION_UDPUDP integrationnoyes

Steps are executed top-to-bottom. The wizard shows an instruction or form to the user, creates a ThingsBoard entity, or both. Output from earlier steps (form values, created entity IDs) becomes available as ${variable} placeholders in later steps.

Shows a markdown page to the user. Use for prerequisites, setup guides, wiring instructions, and post-install verification.

{"type": "SHOW_INSTRUCTION", "name": "Prerequisites", "file": "prerequisites.md"}

Shows a dynamic form. Every field’s key becomes a ${key} variable in all subsequent steps.

{"type": "SHOW_FORM", "name": "Device Settings", "file": "form.json"}

Entity Steps — Create ThingsBoard Resources

Section titled “Entity Steps — Create ThingsBoard Resources”

Each entity step takes a template (a JSON file that matches the ThingsBoard REST API format for that entity) and creates the entity. The created entity’s ID, name, and other properties become available as variables (${device.id}, ${dashboard.name}, etc.).

Step typeCreatesReuses existing by name?
DEVICE_PROFILEDevice profileyes
DEVICEDeviceno (always new)
GATEWAYGateway deviceno (always new)
GATEWAY_CONNECTORGateway connector configurationappended to gateway
DASHBOARDDashboardno (always new)
RULE_CHAINRule chainyes
UPLINK_CONVERTERUplink converter PE only no
DOWNLINK_CONVERTERDownlink converter PE only no
INTEGRATIONIntegration PE only no

Converter / integration ordering. Any UPLINK_CONVERTER or DOWNLINK_CONVERTER step must appear before the INTEGRATION step in the same install method’s array. The validator rejects packages that put converters after the integration.

This ordering is what makes the wiring automatic: the wizard runs the converter steps first, captures their generated IDs as ${uplinkConverter.id} / ${uplinkConverter.name} and ${downlinkConverter.id} / ${downlinkConverter.name}, then resolves those placeholders in the integration template before creating the integration. The creator does not need to wire converter IDs by hand. Referencing ${uplinkConverter.id} and ${downlinkConverter.id} in the integration template is enough; the wizard fills them in at install time.

"installSteps": {
"INTEGRATION_TTN": [
{"type": "SHOW_FORM", "name": "TTN Settings", "file": "ttn/form.json"},
{"type": "DEVICE_PROFILE", "name": "Acme Sensor", "template": "common/device-profile.json"},
{"type": "UPLINK_CONVERTER", "name": "Acme Decoder", "template": "ttn/uplink.json"},
{"type": "DOWNLINK_CONVERTER", "name": "Acme Encoder", "template": "ttn/downlink.json"},
{"type": "INTEGRATION", "name": "${deviceName} - TTN", "template": "ttn/integration.json"},
{"type": "DEVICE", "name": "${deviceName}", "template": "common/device.json"},
{"type": "DASHBOARD", "name": "Monitor", "template": "common/dashboard.json"},
{"type": "SHOW_INSTRUCTION", "name": "Setup Complete", "file": "ttn/post-install.md"}
]
}

For the recommended way to author the converter and integration files, see Integration packages below.

Optional fields on entity steps.

FieldApplies toDescription
serverAttributesAll entity typesPath to a JSON file with key-value pairs saved as server attributes
sharedAttributesAll entity typesPath to a JSON file with key-value pairs saved as shared attributes
credentialsDEVICE, GATEWAYPath to a JSON file with device credentials (see Device credentials)
dockerComposeGATEWAYPath to a YAML template for a custom docker-compose file (see Gateway Docker Compose templates)

Form files are JSON arrays of field definitions. Every key becomes a ${key} variable after the user submits the form.

[
{
"key": "deviceName",
"label": "Device Name",
"type": "STRING",
"defaultValue": "My Sensor",
"required": true,
"helpText": "Name for the device in ThingsBoard"
},
{
"key": "wifiPassword",
"label": "WiFi Password",
"type": "PASSWORD",
"required": true
},
{
"key": "baudRate",
"label": "Baud Rate",
"type": "SELECT",
"defaultValue": "9600",
"options": [
{"value": "9600", "label": "9600"},
{"value": "115200", "label": "115200"}
]
},
{
"key": "loriotServer",
"label": "Loriot server",
"type": "STRING_AUTOCOMPLETE",
"defaultValue": "eu1",
"options": ["eu1", "us1", "as1", "eu2", "ap1"]
},
{
"key": "enableTls",
"label": "Enable TLS",
"type": "BOOLEAN",
"defaultValue": true
},
{
"key": "port",
"label": "Port",
"type": "INTEGER",
"defaultValue": 1883
}
]
TypeRenders asValue type
STRINGText inputstring
PASSWORDPassword input with visibility togglestring
INTEGERNumber inputinteger
BOOLEANCheckboxboolean
SELECTDropdown — value picked from options[].valuestring
STRING_AUTOCOMPLETEFree-text input with autocomplete suggestions from optionsstring
PropertyDescription
keyVariable name (used as ${key} in later steps)
labelLabel shown next to the field
typeOne of the field types above
requiredIf true, the user cannot proceed without a value
defaultValuePre-filled value shown in the field
helpTextText shown when the user clicks the help icon
helpImageImage path shown alongside helpText
optionsSELECT: array of {value, label} entries. STRING_AUTOCOMPLETE: array of plain strings
validatorsArray of regex validators (see below)
secretSupportBoolean. If true, the install dialog renders both a plaintext input and a Secret picker so the user can choose to back the value with a ThingsBoard Secret
secretTypeTEXT or TEXT_FILE. Used by the Secret picker to filter eligible Secrets. Default: TEXT
groupOptional section header. Fields with the same group value render under one heading on the install form
randomGeneratorSTRING/PASSWORD only: if true, shows a regenerate icon that fills the field with a random value
randomSizeSTRING/PASSWORD only: length of the generated value (default: 20)
randomByDefaultSTRING/PASSWORD only: if true, the field is pre-filled with a random value when the form opens (overrides defaultValue)

Reserved keys. These keys are populated by step output and cannot be used as form field keys: uplinkConverter.id, uplinkConverter.name, downlinkConverter.id, downlinkConverter.name, integration.id, integration.name, integration.httpEndpoint. The validator rejects packages that try to define them.

Add regex validators with custom error messages:

{
"key": "devEui",
"label": "Device EUI",
"type": "STRING",
"required": true,
"validators": [
{"pattern": "^[0-9A-Fa-f]{16}$", "message": "Must be exactly 16 hex characters"}
]
}

Multiple validators can be applied to one field — all must pass for the form to be valid.

ThingsBoard PE generates a complete integration package via the Export to IoT Hub button on the integration detail page. The exported ZIP is self-describing: drop the files into your device package and let the install wizard substitute placeholders at install time. Hand-authoring the integration JSON works, but exporting from a real PE integration is the canonical path because it produces the right shape and naming on the first try.

For PE integration install methods (INTEGRATION_LORIOT, INTEGRATION_TTN, INTEGRATION_AWS_IOT, etc.), the package places three files alongside the device-side files:

FilePurpose
integration.jsonIntegration entity JSON, verbatim from PE’s exporter, with ${formKey}, ${secret:NAME;type:TEXT}, and ${alias.prop} placeholders
uplink.jsonUplink converter, verbatim from the exporter
downlink.jsonDownlink converter, verbatim from the exporter

The INTEGRATION step references just one template; the wizard’s SHOW_FORM step still drives the combined form.json:

{"type": "INTEGRATION", "name": "LORIOT Integration", "template": "integration.json"}

The integration type comes from the type field at the top of integration.json. The validator rejects packages with a missing or unknown integration type.

Placeholder Conventions Inside integration.json

Section titled “Placeholder Conventions Inside integration.json”

The exporter emits three kinds of placeholders. Keep them verbatim when copying the export into a package.

PlaceholderMeaning
${formKey}Non-secret form value. The install dialog substitutes the user’s form.json input directly into the body. Examples: ${integrationName}, ${loriotServer}, ${loriotDomain}
${secret:NAME;type:TEXT|TEXT_FILE}Sensitive value. Preserved verbatim in the body. The install dialog creates a Secret named NAME from the user’s form input via POST /api/secret, and ThingsBoard PE resolves the placeholder when the integration starts
${alias.prop}Output from an earlier install step. The wizard captures step outputs and substitutes them into later steps. Examples: ${uplinkConverter.id}, ${downlinkConverter.id}, ${deviceProfile.id}

Form keys that drive a ${secret:…} placeholder receive an 8-character alphanumeric suffix at export time (like loriotToken3KYhD5Wl) to prevent Secret name collisions across separate exports. Do not rename these keys — the placeholder in integration.json and the key field in form.json must match exactly.

Combining Device Fields With Integration Fields

Section titled “Combining Device Fields With Integration Fields”

The wizard renders one form per SHOW_FORM step. Combine the device-side fields and the integration-side fields into a single form.json, and use group to keep the visual sections from the exporter intact:

[
{
"key": "deviceName",
"label": "Device Name",
"type": "STRING",
"defaultValue": "Acme LoRaWAN Sensor",
"required": true
},
{
"key": "integrationName",
"label": "Integration name",
"type": "STRING",
"defaultValue": "Acme LoRaWAN Sensor - LoRaWAN",
"required": true,
"group": "LORIOT Connection"
},
{
"key": "loriotServer",
"label": "LORIOT server",
"type": "STRING_AUTOCOMPLETE",
"defaultValue": "eu1",
"options": ["eu1", "us1", "as1", "eu2", "ap1"],
"required": true,
"group": "LORIOT Connection"
},
{
"key": "loriotAppId",
"label": "Application ID",
"type": "STRING",
"secretSupport": true,
"secretType": "TEXT",
"required": true,
"group": "LORIOT Connection"
},
{
"key": "loriotToken",
"label": "Application Access Token",
"type": "PASSWORD",
"secretSupport": true,
"secretType": "TEXT",
"required": true,
"group": "LORIOT Connection"
}
]

Migration Recipe (Existing Pre-Export Packages)

Section titled “Migration Recipe (Existing Pre-Export Packages)”

For each device package using the older split-artifacts format (integration-template.json + integration-form.json):

  1. Configure a working integration of the same type in PE, then click Export to IoT Hub on the integration detail page. Save the ZIP and unpack it.
  2. Delete the old integration-template.json and integration-form.json. Drop the export’s integration.json into the package directory unchanged.
  3. Replace uplink_data_converter.json with the export’s uplink.json. Replace downlink_data_converter.json with the export’s downlink.json (if the package supports downlink).
  4. Merge form.json. Keep the device-side entries (like deviceName) at the top, then append the export’s entries. Use group fields to preserve the visual layout.
  5. Update device-info.json. The INTEGRATION step now references a single template (no form field). Converter step template paths point at uplink.json and downlink.json (no _data_converter suffix).
  6. Smoke-test the package end-to-end via the install dialog. Page 1 shows the combined form with grouped sections, page 2 provisions the entities, and page 3 confirms success.

Entity templates are JSON files that match the ThingsBoard REST API format for that entity. Export an entity from ThingsBoard, add ${variable} placeholders for any values that must be dynamic, and save the result.

{
"name": "Acme Temperature Sensor",
"type": "DEFAULT",
"transportType": "MQTT",
"description": "Device profile for Acme Temperature Sensor with MQTT transport",
"profileData": {
"configuration": {"type": "DEFAULT"},
"transportConfiguration": {
"type": "MQTT",
"deviceTelemetryTopic": "v1/devices/me/telemetry",
"deviceAttributesTopic": "v1/devices/me/attributes"
}
}
}

Always reference the created device profile via ${deviceProfile.id}:

{
"name": "${deviceName}",
"type": "My Sensor Type",
"label": "${deviceName}",
"deviceProfileId": {
"id": "${deviceProfile.id}",
"entityType": "DEVICE_PROFILE"
}
}

Export a dashboard from ThingsBoard (Dashboards → Export), then replace any hardcoded device UUIDs in entity aliases with ${device.id}:

{
"title": "Acme Temperature Sensor Monitor",
"configuration": {
"entityAliases": {
"device_alias": {
"alias": "Device",
"filter": {
"type": "singleEntity",
"singleEntity": {
"id": "${device.id}",
"entityType": "DEVICE"
}
}
}
}
}
}

For boolean and integer values, use bare ${variable} (no quotes):

{
"enableTls": ${enableTls},
"port": ${mqtt.port}
}

For string values inside quotes, use ${variable} as normal:

{
"name": "${deviceName}",
"baseUrl": "${chirpstackUrl}"
}

By default, ThingsBoard auto-generates an access token for each new device, available as ${device.token}. This works for most use cases and requires no credentials file.

To override the default, add a credentials field to the DEVICE or GATEWAY step pointing to a credentials JSON file:

{
"type": "DEVICE",
"name": "${deviceName}",
"template": "device.json",
"credentials": "credentials.json"
}
{
"credentialsType": "MQTT_BASIC",
"credentialsValue": {
"clientId": "${deviceName}",
"userName": "${mqttUsername}",
"password": "${mqttPassword}"
}
}
{
"credentialsType": "ACCESS_TOKEN",
"credentialsValue": "${customToken}"
}
{
"credentialsType": "X509_CERTIFICATE",
"credentialsValue": "${deviceCertificate}"
}

By default, ${gateway.downloadButton} in post-install markdown generates a standard docker-compose.yml from the ThingsBoard backend with the gateway’s host, port, and access token pre-filled.

To provide a custom template with additional ports, volumes, environment variables, or credentials, add a dockerCompose field to the GATEWAY step:

{
"type": "GATEWAY",
"name": "${deviceName} Gateway",
"template": "gateway.json",
"dockerCompose": "docker-compose.yml"
}

The template is a standard YAML file with ${variable} placeholders. All variables (form values, transport config, entity outputs) are resolved after the gateway is created:

services:
tb-gateway:
image: thingsboard/tb-gateway:3.7-stable
restart: always
ports:
- "5026:5026" # Modbus TCP
- "5000:5000" # REST connector
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- host=${mqtt.host}
- port=${mqtt.port}
- accessToken=${gateway.token}
volumes:
- tb-gw-config:/thingsboard_gateway/config
- tb-gw-logs:/thingsboard_gateway/logs
- tb-gw-extensions:/thingsboard_gateway/extensions
volumes:
tb-gw-config:
tb-gw-logs:
tb-gw-extensions:
  • Your gateway needs extra ports (e.g. Modbus TCP, BACnet, Socket connector).
  • You need additional volumes or bind mounts.
  • You want custom environment variables beyond host, port, and accessToken.
  • Your gateway uses MQTT Basic credentials and needs to pass clientId, userName, password.
services:
tb-gateway:
image: thingsboard/tb-gateway:3.7-stable
environment:
- host=${mqtt.host}
- port=${mqtt.port}
- clientId=${mqttClientId}
- username=${mqttUsername}
- password=${mqttPassword}

When a dockerCompose field is present on the GATEWAY step:

  1. After the gateway is created, the template file is read from the ZIP.
  2. All ${variable} placeholders are resolved.
  3. ${gateway.downloadButton} in post-install markdown serves this custom file as a direct download.
  4. No backend API call is made — the file is generated entirely in the browser.

When no dockerCompose field is set, the download button falls back to the standard ThingsBoard-generated docker-compose.yml.

## Launch Gateway
Download the configuration file and start the gateway:
${gateway.downloadButton}
Then run in the same directory:
```bash
docker compose up -d
```
The gateway will connect to **${mqtt.host}:${mqtt.port}**.

Instruction files use standard Markdown with two extensions: ${variable} placeholders and image-gallery helpers.

## Prerequisites
Before you begin, make sure you have:
- **ESP32 Dev Kit** board
- **USB cable** (micro-USB)
- **Arduino IDE** installed
### Step 1: Connect the Board
Connect the board to your computer via USB as shown:
![Wiring Diagram](images/wiring-diagram.png)
> **Note:** Make sure the USB cable supports data transfer, not just charging.

Variables are resolved before rendering. Use them to show user-provided values, created resource names, and platform settings:

## Setup Complete
Your device **${deviceName}** is ready!
Use this token in your firmware:
${device.token}
Open the [${dashboard.name}](${dashboard.url}) dashboard to see telemetry.
The device is connected to **${mqtt.host}:${mqtt.port}** via MQTT.

See the full Variable reference for all available variables.

Variables are resolved inside fenced code blocks, which is especially useful for firmware snippets:

```cpp
#define WIFI_SSID "${wifiSsid}"
#define WIFI_PASSWORD "${wifiPassword}"
#define TOKEN "${device.token}"
#define TB_SERVER "${mqtt.host}"
#define TB_PORT ${mqtt.port}
```
Terminal window
curl -o docker-compose.yml "${gateway.dockerComposeUrl}"
docker compose up -d

Place images anywhere in the ZIP — the images/ folder is recommended.

my-device.zip
├── images/
│ ├── device-photo.jpg
│ ├── wiring-diagram.png
│ └── serial-output.png
├── prerequisites.md
└── post-install.md

Supported formats: .png, .jpg, .jpeg, .gif, .svg.

Use standard Markdown with paths relative to the ZIP root:

![Wiring Diagram](images/wiring-diagram.png)

The wizard resolves these paths to inline base64 data URIs from the ZIP — no external hosting needed.

Display multiple images side-by-side as clickable thumbnails using the gallery helper:

${images.gallery(images/step1-tools.png, images/step2-upload.png, images/step3-verify.png)}
  • Paths are comma-separated, relative to the ZIP root.
  • Images render as a horizontal row of thumbnails.
  • Clicking a thumbnail expands it full-size; clicking again collapses.
  • Useful for multistep visual instructions (e.g. IDE menu navigation screenshots).

Form fields can show a help image alongside the help text:

{
"key": "devEui",
"label": "Device EUI",
"type": "STRING",
"required": true,
"helpText": "16 hex characters from the device label",
"helpImage": "images/device-label-eui.png"
}

The user clicks a help icon next to the field to expand the image.

  • Keep images under 500 KB each — the whole ZIP is stored as a single file.
  • PNG for screenshots and diagrams; JPG for device photos.
  • Crop tightly to the relevant area.
  • Annotate with arrows or highlights to point out important UI elements.
  • Use descriptive filenames: chirpstack-api-key.png, not screenshot1.png.

overview.md is an optional file at the ZIP root that provides the full description for your device’s detail page on IoT Hub. It complements the short description in device-info.json:

  • description in device-info.json → shown on browse cards (short, max 512 chars).
  • overview.md → shown on the device detail page; pre-fills the “Readme” step in the upload wizard.

Use it to describe hardware specifications, features, and any context a user needs before deciding to install. Standard Markdown is supported, including tables and images.

## ESP32 Dev Kit C V4
The ESP32 Dev Kit C V4 is a compact development board built on the ESP-WROOM-32U module,
featuring a dual-core processor with Wi-Fi and Bluetooth connectivity.
### Features
- Dual-core Xtensa LX6 processor (240 MHz)
- 520 KB SRAM, 4 MB Flash
- Wi-Fi 802.11 b/g/n + Bluetooth 4.2 + BLE
- 34 programmable GPIO pins
- Multiple interfaces: SPI, I2C, UART, ADC, DAC, PWM
### Specifications
| Parameter | Value |
|-----------|-------|
| Operating Voltage | 3.3V |
| Input Voltage | 5V (USB) |
| Clock Speed | 80–240 MHz |
| Flash | 4 MB |

Variables are resolved at the time of step execution and are available in all subsequent markdown files, entity templates, and form help text.

Every form field key is exposed as ${key}. Common examples:

ExampleTypical use
${deviceName}Device name entered by the user
${wifiSsid}Wi-Fi network name
${wifiPassword}Wi-Fi password
${devEui}LoRaWAN Device EUI
${chirpstackUrl}ChirpStack server URL
${mqttUsername}MQTT Basic username
${mqttPassword}MQTT Basic password
VariableStep typeDescription
${deviceProfile.id}DEVICE_PROFILECreated profile UUID
${deviceProfile.name}DEVICE_PROFILEProfile name
${device.id}DEVICECreated device UUID
${device.name}DEVICEDevice name
${device.token}DEVICEDevice access token
${device.url}DEVICELink to the device page in ThingsBoard
${gateway.id}GATEWAYGateway UUID
${gateway.name}GATEWAYGateway name
${gateway.token}GATEWAYGateway access token
${gateway.downloadButton}GATEWAYDownload button for docker-compose.yml
${dashboard.id}DASHBOARDDashboard UUID
${dashboard.name}DASHBOARDDashboard name
${dashboard.url}DASHBOARDLink to the dashboard
${ruleChain.id}RULE_CHAINRule chain UUID
${uplinkConverter.id}UPLINK_CONVERTERUplink converter UUID PE only . Auto-substituted into the integration template.
${uplinkConverter.name}UPLINK_CONVERTERUplink converter name
${downlinkConverter.id}DOWNLINK_CONVERTERDownlink converter UUID PE only . Auto-substituted into the integration template.
${downlinkConverter.name}DOWNLINK_CONVERTERDownlink converter name
${integration.id}INTEGRATIONIntegration UUID PE only
${integration.name}INTEGRATIONIntegration name PE only
${integration.httpEndpoint}INTEGRATIONThe webhook URL the integration listens on PE only . Reference it in post-install.md to display the URL the customer registers with their LoRaWAN provider, MQTT broker, etc.

These come from the ThingsBoard platform settings of the installing instance:

VariableDefaultDescription
${mqtt.host}from baseUrlMQTT broker host
${mqtt.port}1883MQTT broker port
${mqtts.port}8883MQTTS (TLS) port
${http.host}from baseUrlHTTP API host
${http.port}8080HTTP API port
${coap.host}from baseUrlCoAP host
${coap.port}5683CoAP port

Two placeholders render as inline buttons that link to the device’s manufacturer resources. They work in overview.md and in any SHOW_INSTRUCTION markdown.

VariableSource field in device-info.jsonRenders as
${product.button}productURLButton labeled {name} Product page
${datasheet.button}datasheetURLButton labeled {name} Datasheet

If the corresponding URL is not set, the placeholder is silently dropped — so it is safe to leave them in your markdown even when only one URL is provided.

## Device Technical Documentation
Download datasheets and manuals for the FMC130.
${product.button} ${datasheet.button}

Before uploading your ZIP, verify each item below.

When you upload a new version, pair the version bump with a changelog entry — a short note that tells users exactly what changed since the previous version, so they can decide whether, and how, to upgrade.

Summarize what changed since the previous version: new features, bug fixes, breaking changes, and any migration notes users need to know.

Group your entry under these headings, and include only the ones that apply:

HeadingWhat goes here
New featuresNew capabilities, configuration options, or behavior added in this version
Bug fixesDefects corrected since the previous version — describe the symptom users saw, not the internal cause
Breaking changesAnything that changes existing behavior in a way that can disrupt an installed copy — renamed keys, removed options, changed defaults
Migration notesThe concrete steps an existing user must take to move from the previous version to this one
  • Lead with the user impact. Describe what the user can now do, or what no longer breaks — not how you implemented it.
  • Be specific. Name the exact keys, fields, settings, or outputs that changed. “Renamed the output key from temp to temperature” is actionable; “improved naming” is not.
  • One change per bullet. Keep each item to a single, scannable line.
  • Flag breaking changes loudly. If an upgrade can disrupt an installed copy, say so explicitly and pair it with a migration note.

Pair every entry with a semantic version bump — patch (1.0.1) for fixes, minor (1.1.0) for backward-compatible features, major (2.0.0) for breaking changes.

## 2.0.0
### Breaking changes
- Renamed the output key from `temp` to `temperature` to match the
ThingsBoard telemetry convention.
### Migration notes
- Update any dashboards, alarm rules, or downstream calculated fields
that read the `temp` key to read `temperature` instead.
### New features
- Added an optional `humidity` argument; when present, the formula
now also computes a `dewPoint` output.
### Bug fixes
- Fixed missing output when the input telemetry arrived as a string
instead of a number.

Once you complete the upload wizard and click Submit, your version enters the IoT Hub review queue. The ThingsBoard team checks every submission before it goes live.

To see the current status of your submission, open the Creator Portal and go to Items. Find your item in the list and click the Manage Versions icon in its row. The Versions page lists every version you have uploaded with a Status column that updates in real time.

StatusMeaning
Pending ReviewYour version is in the review queue and has not been evaluated yet
ApprovedYour version passed review and is now live on IoT Hub
RejectedYour version did not pass review — see the reviewer comment for details

Reviewers verify that the submission meets the same criteria as the Pre-Upload Checklist: the package installs cleanly, entity templates are correct, instructions are complete and accurate, images are present, and the listing and readme give users enough context to evaluate and use the device.

The Status column will show Rejected. Open the version details to read the reviewer’s comment, which explains specifically what needs to be fixed.

To resubmit:

  1. Fix the reported issues in your local package.
  2. Return to Items → Manage Versions for your item.
  3. Click + Upload new version and complete the wizard with the corrected package.