> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# balena

> Client-mode REST API connector for balenaCloud: find and inspect fleet devices, pin a device or a whole fleet to a release, read and write device tags and environment variables, list fleets and releases, and restart apps or services, blink, reboot or force-update devices through the Supervisor API proxy

Technical Preview

## Overview

One client-mode connector, **Balena**, for [balenaCloud](https://www.balena.io/cloud) (`https://api.balena-cloud.com`). It covers two APIs that share the same host and the same API key:

* the **balena API v7**, an [OData](https://docs.balena.io/reference/api/overview) interface over fleets (called `application` in the API), devices, releases, tags and variables;
* the **Supervisor API proxy** (`/supervisor/...`), which forwards a command to the balena Supervisor on one device, identified by its UUID.

Use it to let the broker manage the edge gateways it runs next to: watch device health, restart a stuck container when a line alarm fires, pin a fleet to a known-good release before a shift, or tag devices with their UNS location.

| Event | Request | Notes |
| - | - | - |
| ListDevices | `GET /v7/device?{odata}` | Pass the whole OData query string in `odata` |
| GetDevice | `GET /v7/device(uuid='{uuid}')?$select=...` | Health, status, `overall_status`, running and pinned release |
| UpdateDevice | `PATCH /v7/device(uuid='{uuid}')` | Pin to a release, rename, add a note; returns `OK` |
| ListDeviceTags | `GET /v7/device_tag?$filter=device/uuid eq '{uuid}'` | |
| CreateDeviceTag | `POST /v7/device_tag` | Body: `device` (numeric id), `tag_key`, `value` |
| UpdateDeviceTag | `PATCH /v7/device_tag(device={device_id},tag_key='{tag_key}')` | Body: `value` |
| ListDeviceEnvVars | `GET /v7/device_environment_variable?$filter=...` | Device-level variables by UUID |
| CreateDeviceEnvVar | `POST /v7/device_environment_variable` | Body: `device`, `name`, `value` |
| ListFleets | `GET /v7/application?$filter=is_directly_accessible_by__user/any(dau:true)` | Fleets the key can reach |
| GetFleet | `GET /v7/application(slug='{fleet_slug}')` | Target release and tracking flag |
| UpdateFleet | `PATCH /v7/application(slug='{fleet_slug}')` | Pin the whole fleet to a release |
| ListReleases | `GET /v7/release?$filter=belongs_to__application eq {fleet_id}` | Latest 10 releases |
| SupervisorRestartApp | `POST /supervisor/v1/restart` | Restart all containers of the app |
| SupervisorRestartService | `POST /supervisor/v2/applications/{app_id}/restart-service` | Restart one service |
| SupervisorBlink | `POST /supervisor/v1/blink` | Blink the device LED for 15 seconds (identify device), if it has one |
| SupervisorReboot | `POST /supervisor/v1/reboot` | Reboot the device |
| SupervisorUpdate | `POST /supervisor/v1/update` | Check for updates now (`force` skips update locks) |
| SupervisorDeviceState | `POST /supervisor/v1/device` | Supervisor's view of the device (status, commit, update pending) |

Topics use `cloud-iot/balena/<object>/<verb>`; replies land on `<topic>/response`. For openBalena, change `BASE_ADDRESS` to your own API host, for example `https://api.balena.example.com`.

> [!WARNING]
>
> Never commit an API key. Store it as a broker secret (for example `BALENA_API_KEY`) and map it into payloads with `GET SECRET`. A balena API key acts with your full user permissions, and the API has no undo: test every `$filter` with a GET before you use it in a PATCH.

## Authentication

Every event sends `Authorization: Bearer <key>`. Use a **named API key**: in the balenaCloud dashboard open **Preferences**, then **Access tokens**, and create an API key, or run `balena api-key generate coreflux` with the balena CLI. Session tokens also work but expire.

Pass the key as `api_key` on every payload. A missing or invalid key returns `401` on device, tag, variable, fleet-write and Supervisor events. Fleets and releases are publicly readable resources, so without a valid key `GetFleet` and `ListReleases` return `200` with an empty `d` list for private fleets, and `ListFleets` returns `400 Missing binding: @__ACTOR_ID`. Treat an empty `d` on those events as a possible auth problem.

## Basic Syntax

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Balena WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://api.balena-cloud.com"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '30'
        WITH DEFAULT_CONTENT_TYPE "application/json"
        # Smoke: ListFleets (GET) then GetDevice (GET), UpdateDevice (PATCH tag/note) and SupervisorBlink (POST).
    ADD EVENT ListDevices
        WITH SOURCE_TOPIC "cloud-iot/balena/device/list"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/device?{value.json.odata}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT GetDevice
        WITH SOURCE_TOPIC "cloud-iot/balena/device/get"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/device(uuid='{value.json.uuid}')?$select=id,uuid,device_name,status,overall_status,is_online,last_connectivity_event,os_version,supervisor_version,cpu_temp,cpu_usage,memory_usage,memory_total,storage_usage,storage_total,is_running__release,is_pinned_on__release,should_be_running__release"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT UpdateDevice
        WITH SOURCE_TOPIC "cloud-iot/balena/device/update"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/v7/device(uuid='{value.json.uuid}')"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT ListDeviceTags
        WITH SOURCE_TOPIC "cloud-iot/balena/device-tag/list"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device-tag/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/device_tag?$filter=device/uuid%20eq%20'{value.json.uuid}'"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT CreateDeviceTag
        WITH SOURCE_TOPIC "cloud-iot/balena/device-tag/create"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device-tag/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v7/device_tag"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT UpdateDeviceTag
        WITH SOURCE_TOPIC "cloud-iot/balena/device-tag/update"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device-tag/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/v7/device_tag(device={value.json.device_id},tag_key='{value.json.tag_key}')"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT ListDeviceEnvVars
        WITH SOURCE_TOPIC "cloud-iot/balena/device-env/list"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device-env/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/device_environment_variable?$filter=device/any(d:d/uuid%20eq%20'{value.json.uuid}')"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT CreateDeviceEnvVar
        WITH SOURCE_TOPIC "cloud-iot/balena/device-env/create"
        WITH DESTINATION_TOPIC "cloud-iot/balena/device-env/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v7/device_environment_variable"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT ListFleets
        WITH SOURCE_TOPIC "cloud-iot/balena/fleet/list"
        WITH DESTINATION_TOPIC "cloud-iot/balena/fleet/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/application?$filter=is_directly_accessible_by__user/any(dau:true)&$select=id,app_name,slug,should_be_running__release,should_track_latest_release"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT GetFleet
        WITH SOURCE_TOPIC "cloud-iot/balena/fleet/get"
        WITH DESTINATION_TOPIC "cloud-iot/balena/fleet/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/application(slug='{value.json.fleet_slug}')?$select=id,app_name,slug,should_be_running__release,should_track_latest_release"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT UpdateFleet
        WITH SOURCE_TOPIC "cloud-iot/balena/fleet/update"
        WITH DESTINATION_TOPIC "cloud-iot/balena/fleet/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/v7/application(slug='{value.json.fleet_slug}')"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT ListReleases
        WITH SOURCE_TOPIC "cloud-iot/balena/release/list"
        WITH DESTINATION_TOPIC "cloud-iot/balena/release/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v7/release?$filter=belongs_to__application%20eq%20{value.json.fleet_id}&$select=id,commit,semver,raw_version,status,created_at&$orderby=created_at%20desc&$top=10"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
    ADD EVENT SupervisorRestartApp
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/restart"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/restart/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v1/restart"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT SupervisorRestartService
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/restart-service"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/restart-service/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v2/applications/{value.json.app_id}/restart-service"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT SupervisorBlink
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/blink"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/blink/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v1/blink"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT SupervisorReboot
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/reboot"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/reboot/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v1/reboot"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT SupervisorUpdate
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/update"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/update/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v1/update"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
    ADD EVENT SupervisorDeviceState
        WITH SOURCE_TOPIC "cloud-iot/balena/supervisor/device"
        WITH DESTINATION_TOPIC "cloud-iot/balena/supervisor/device/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/supervisor/v1/device"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.api_key}"
        WITH BODY "{value.json.body}"
```

## MQTT examples

Request bodies go under `body`, so control fields like `api_key`, `uuid` and `fleet_slug` are not sent to balena.

**Find the devices of a fleet**: publish to `cloud-iot/balena/device/list`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_key": "<balena API key>",
  "odata": "$filter=belongs_to__application/any(a:a/slug%20eq%20'coreflux/line-1')&$select=uuid,device_name,is_online,overall_status"
}
```

The reply is `{"d": [ ... ]}`. Values inside `odata` must be URL-encoded: write spaces as `%20`.

**Check one device**: publish `{"api_key": "<key>", "uuid": "<device uuid>"}` to `cloud-iot/balena/device/get`. The reply includes `is_online`, `overall_status`, CPU temperature, memory and storage use, and the running, pinned and target releases. Use the full 32-character UUID.

**Pin a device to a release**: publish to `cloud-iot/balena/device/update`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_key": "<balena API key>",
  "uuid": "<device uuid>",
  "body": {"is_pinned_on__release": 3456789}
}
```

Send `{"is_pinned_on__release": null}` to make the device track its fleet again. The same event renames a device (`{"device_name": "press-07"}`) or sets a note (`{"note": "line 1 maintenance"}`). A successful PATCH returns `200` with the text `OK`.

**Pin the whole fleet**: publish to `cloud-iot/balena/fleet/update`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_key": "<balena API key>",
  "fleet_slug": "coreflux/line-1",
  "body": {"should_be_running__release": 3456789, "should_track_latest_release": false}
}
```

Get release ids from `cloud-iot/balena/release/list` with `{"api_key": "<key>", "fleet_id": 1234}`, and the fleet id from `cloud-iot/balena/fleet/get`.

**Tag a device with its UNS location**: publish to `cloud-iot/balena/device-tag/create`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_key": "<balena API key>",
  "body": {"device": 1234567, "tag_key": "uns_path", "value": "porto/line1/press-07"}
}
```

`device` is the numeric device id from `GetDevice`, not the UUID. Change an existing tag with `cloud-iot/balena/device-tag/update` and `{"device_id": 1234567, "tag_key": "uns_path", "body": {"value": "..."}}`; creating the same key twice returns `409`.

**Set a device environment variable**: publish to `cloud-iot/balena/device-env/create` with `{"api_key": "<key>", "body": {"device": 1234567, "name": "MQTT_BROKER", "value": "mqtt://broker.local:1883"}}`. The services on that device restart to pick up the new value.

**Restart one service on a device**: publish to `cloud-iot/balena/supervisor/restart-service`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_key": "<balena API key>",
  "app_id": 1234,
  "body": {"uuid": "<device uuid>", "data": {"serviceName": "collector"}}
}
```

The other Supervisor events take `{"uuid": "<device uuid>"}` as the body: blink and reboot as is, `SupervisorRestartApp` with `"data": {"appId": 1234}`, `SupervisorUpdate` with `"data": {"force": true}` and `SupervisorDeviceState` with `"method": "GET"`. The device must be online and connected to the balena VPN.

## Helper actions

Keep the API key in a broker secret, poll fleet health on a timer, and let operators or alarms trigger restarts and release pins with small payloads:

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION BalenaPollFleetHealth
ON EVERY 5 MINUTES DO
    SET "key" WITH GET SECRET "BALENA_API_KEY"
    PUBLISH TOPIC "cloud-iot/balena/device/list" WITH { "api_key": {key}, "odata": "$filter=belongs_to__application/any(a:a/slug%20eq%20'coreflux/line-1')&$select=uuid,device_name,is_online,overall_status,cpu_temp,memory_usage,memory_total" }

DEFINE ACTION BalenaRestartServiceOnRequest
ON TOPIC "ops/balena/restart-service" DO
    SET "key" WITH GET SECRET "BALENA_API_KEY"
    SET "uuid" WITH GET JSON "uuid" IN PAYLOAD AS STRING
    SET "app" WITH GET JSON "app_id" IN PAYLOAD AS STRING
    SET "service" WITH GET JSON "service" IN PAYLOAD AS STRING
    PUBLISH TOPIC "cloud-iot/balena/supervisor/restart-service" WITH { "api_key": {key}, "app_id": {app}, "body": { "uuid": {uuid}, "data": { "serviceName": {service} } } }

DEFINE ACTION BalenaPinFleetOnRequest
ON TOPIC "ops/balena/pin-fleet" DO
    SET "key" WITH GET SECRET "BALENA_API_KEY"
    SET "slug" WITH GET JSON "fleet_slug" IN PAYLOAD AS STRING
    SET "release" WITH GET JSON "release_id" IN PAYLOAD AS STRING
    PUBLISH TOPIC "cloud-iot/balena/fleet/update" WITH { "api_key": {key}, "fleet_slug": {slug}, "body": { "should_be_running__release": {release}, "should_track_latest_release": false } }
```

`BalenaPollFleetHealth` publishes the device list of `coreflux/line-1` to `cloud-iot/balena/device/list/response` every five minutes; feed it into a model or dashboard. Restrict who can publish to `ops/balena/#` and read `cloud-iot/balena/#` with a rule: these topics can reboot devices and their payloads carry the API key.

## Notes

* Errors come back on the response topic: `401` for a missing or invalid key, `400` for a bad OData query or unknown field, an empty `d` list when a read matches no device or fleet, `409` for a duplicate tag key or variable name, and `429` with a `Retry-After` header when rate limited. Supervisor events fail when the device is offline or off the VPN.
* `GetDevice` names `overall_status` in `$select` on purpose: balena only returns that field when it is requested.
* Fleet slugs look like `org/fleet` and go inside the quotes as is.
* The Supervisor restart-service call through the cloud proxy follows the documented proxy pattern (`/supervisor/<path>` with `uuid` and `data`); balena documents the local form of that endpoint.
* Deleting devices, fleets, tags or variables, moving devices between fleets, OS and Supervisor upgrades, service and fleet variables, and public device URLs are not part of this template.

## Related

* [REST API Connectors](/latest/lot-language/routes/rest-api-routes)
* [REST API app templates](/latest/lot-language/routes/rest-api-apps/index)
* [Azure IoT Hub](/latest/lot-language/routes/rest-api-apps/azure-iot-hub)
* [AWS IoT Core](/latest/lot-language/routes/rest-api-apps/aws-iot-core)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.