Elixir client for the balena Supervisor HTTP API.
Call the Supervisor from a service on the device (local HTTP API), or from your laptop / CI through the balenaCloud Supervisor proxy.
| Hex | ex_balena (0.1.3) |
| Docs | hexdocs.pm/ex_balena |
| Source | basement-lab/ex_balena |
| License | MIT |
| This package | Not this package |
|---|---|
balena Supervisor HTTP API (/v1, /v2, /ping) |
balenaCloud OpenAPI / SDK (fleets, releases, devices, billing, …) |
| Device agent control (state, restart, reboot, host-config, …) | Full balena platform management |
| On-device or remote proxy access | A complete typed SDK of every Supervisor route (yet) |
Official reference (source of truth):
- Supervisor API docs
- supervisor-api.md
- Upstream agent: balena-os/balena-supervisor
There are three independent version axes. Mixing them up is the usual source of “works on my device / fails on yours.”
| Package | Status |
|---|---|
0.1.3 (current) |
Thin Tesla client, dual-mode auth, generic v1/v2 GET/POST, a few named helpers |
Always pin with ~> in mix.exs so you get compatible patch releases only.
The Supervisor documents routes under:
| Prefix | Role |
|---|---|
/ping |
Unversioned liveness |
/v1/... |
Original device/control endpoints (blink, reboot, device state, host-config, single-container app helpers, …) |
/v2/... |
Multi-container / newer surface (applications state, service actions, state status, local mode, journal, device tags, …) |
Balena’s docs still note historically that “the API is versioned (currently at v1)”; in practice both v1 and v2 are first-class in the same reference page. Prefer v2 for multi-container fleets; many v1 app routes return 400 on multi-container devices.
Each endpoint (and some response fields) has a minimum supervisor version. The device’s running agent—not the Hex package—decides whether a call succeeds.
| Fact | Value (verified 2026-08-06) |
|---|---|
| Latest published agent | balena-supervisor v19.0.5 (released 2026-08-05) |
| Baseline HTTP API | Supervisor ≥ 1.1.0 (OS images after 2015-10-14) |
BALENA_* env names |
Prefer these; devices on supervisor < 7.22.0 used RESIN_* |
| How to check on a device | GET /v2/version (since supervisor v7.21) or dashboard / host OS |
| Endpoint / feature | Min supervisor |
|---|---|
| HTTP API present | 1.1.0 |
GET /v1/device |
1.6 |
GET/POST /v1/apps/:appId... |
1.8 |
GET /v1/healthy |
6.5 |
GET/PATCH /v1/device/host-config |
6.6 |
/v2/applications/:appId/{restart,purge,*-service} |
7.0 (serviceName body field: 8.2.2+) |
GET /v2/applications/state (+ per-app state) |
7.12 |
GET /v2/version, local-mode routes |
7.21 |
GET /v2/containerId |
8.6 |
GET /v2/state/status |
9.7 |
GET /v2/device/name, GET /v2/device/tags |
9.11 |
GET /v2/cleanup-volumes |
10.0 |
POST /v2/journal-logs |
10.2 (format: 10.3; since/until: 14.7.0) |
GET /v2/device/vpn |
11.4 |
| Host-config respects update locks (PATCH) | 12.11.34+ (with balenaOS 2.82.6+) |
host-config proxy dns (dnsu2t) |
16.6.0 |
updateStatus / appUuid / service image on applications state |
17.7.0 |
PATCH /v2/device/tags |
17.8 |
ExBalena does not negotiate these versions for you. If you call an endpoint newer than the device supervisor, you will get HTTP errors from the agent.
Checked 2026-08-06 against Supervisor API and agent v19.0.5.
A practical HTTP foundation:
- Dual-mode Tesla client (on-device apikey or remote Bearer token)
- Finch pool under
ExBalena.Supervisor - Generic
GET/POSTfor/v1and/v2 - Named helpers for the most common “is it up / what’s running?” flows
| Gap | Detail |
|---|---|
| Full named API | Most official routes lack first-class functions |
| PATCH | Required for host-config and device tags; not exposed on helpers |
| Typed models | No request/response structs matching Supervisor JSON |
| Uniform results | V1 returns Tesla results; V2 maps some statuses; 202/204/streams not fully normalized |
| Remote proxy wrappers | Only Balena.current_state/1 builds {uuid, method, ...}; generic calls do not |
Legend: ✅ named helper · 🟡 via ExBalena.API.V1|V2 get/post (you supply path/body) · ❌ needs PATCH or streaming support beyond current helpers · ⚪ local-mode / CLI-oriented
| Method | Path | Min SV | Coverage |
|---|---|---|---|
| GET | /ping |
1.1.0 | 🟡 |
| POST | /v1/blink |
— | 🟡 |
| POST | /v1/update |
— | 🟡 |
| POST | /v1/reboot |
— | 🟡 |
| POST | /v1/shutdown |
— | 🟡 |
| POST | /v1/purge |
— | 🟡 |
| POST | /v1/restart |
— | 🟡 |
| POST | /v1/regenerate-api-key |
— | 🟡 |
| GET | /v1/device |
1.6 | 🟡 |
| POST | /v1/apps/:appId/stop |
1.8 | 🟡 |
| POST | /v1/apps/:appId/start |
1.8 | 🟡 |
| GET | /v1/apps/:appId |
1.8 | 🟡 |
| GET | /v1/healthy |
6.5 | ✅ ExBalena.healthy/0 |
| PATCH | /v1/device/host-config |
6.6 | ❌ |
| GET | /v1/device/host-config |
6.6 | 🟡 |
| Method | Path | Min SV | Coverage |
|---|---|---|---|
| GET | /v2/applications/state |
7.12 | ✅ Balena.current_state/0 · remote current_state/1 |
| GET | /v2/applications/:appId/state |
7.12 | 🟡 |
| GET | /v2/state/status |
9.7 | 🟡 |
| POST | /v2/applications/:appId/restart-service |
7.0 | 🟡 |
| POST | /v2/applications/:appId/stop-service |
7.0 | 🟡 |
| POST | /v2/applications/:appId/start-service |
7.0 | 🟡 |
| POST | /v2/applications/:appId/restart |
7.0 | 🟡 |
| POST | /v2/applications/:appId/purge |
7.0 | 🟡 |
| GET | /v2/version |
7.21 | 🟡 |
| GET | /v2/containerId |
8.6 | 🟡 |
| GET/POST | /v2/local/target-state |
7.21 | ⚪ / 🟡 |
| GET | /v2/local/device-info |
7.21 | ⚪ / 🟡 |
| GET | /v2/local/logs |
7.21 | ⚪ stream |
| GET | /v2/device/name |
9.11 | 🟡 |
| GET | /v2/device/tags |
9.11 | 🟡 |
| PATCH | /v2/device/tags |
17.8 | ❌ |
| GET | /v2/device/vpn |
11.4 | 🟡 |
| GET | /v2/cleanup-volumes |
10.0 | 🟡 |
| POST | /v2/journal-logs |
10.2 | 🟡 / stream caveats |
def deps do
[
{:ex_balena, "~> 0.1.3"}
]
endmix deps.get# application.ex
children = [
ExBalena.Supervisor
# ...
]Or dynamically:
{:ok, _pid} = ExBalena.Supervisor.start_link()
# equivalent:
:ok = ExBalena.start()Label services that need the API:
services:
my-service:
labels:
io.balena.features.supervisor-api: "1"| Variable | Used for |
|---|---|
BALENA |
Client treats "1" / "true" as on-device mode |
BALENA_SUPERVISOR_ADDRESS |
Base URL, e.g. http://127.0.0.1:48484 |
BALENA_SUPERVISOR_API_KEY |
Query apikey (required for all routes except /ping) |
BALENA_APP_ID |
Fleet id for many v1/v2 paths |
Supervisor < 7.22.0: use RESIN_* names instead of BALENA_*.
| Source | Used for |
|---|---|
~/.balena/token |
Bearer token (balena login) |
Device uuid / deviceId / fleet appId |
Proxy targeting |
Remote base URL used by this client:
https://api.balena-cloud.com/supervisor
Official proxy: POST /supervisor/<path> with JSON including uuid or deviceId or appId, optional method (default POST), optional data for the supervisor body. See Balena’s remote examples in the Supervisor API docs.
ExBalena.healthy()
# GET /v1/healthy# On device
Balena.current_state()
# GET /v2/applications/state
# Remote proxy
Balena.current_state("your-device-uuid")
# POST .../supervisor/v2/applications/state
# body: %{"uuid" => "...", "method" => "GET"}Fields such as updateStatus, appUuid, and service image require supervisor ≥ 17.7.0.
ExBalena.API.V1.get("/device")
ExBalena.API.V1.post("/reboot", %{})
ExBalena.API.V1.post("/update", %{"force" => true})
ExBalena.API.V2.get("/version")
ExBalena.API.V2.get("/state/status")
ExBalena.API.V2.post("/applications/#{app_id}/restart-service", %{
"serviceName" => "main",
"force" => false
})Paths are appended after /v1 or /v2. Bodies are JSON maps.
Remote caveat: generic
get/1andpost/2use the remote base URL and Bearer token, but do not auto-wrap{uuid, method, data}. UseBalena.current_state/1as a pattern, or build the proxy body yourself.
client = ExBalena.Client.new()
Tesla.get(client, "/v2/version")Mode selection inside ExBalena.Client:
| Condition | Base URL | Auth |
|---|---|---|
BALENA in {"1","true"} |
BALENA_SUPERVISOR_ADDRESS |
?apikey= |
| otherwise | https://api.balena-cloud.com/supervisor |
Authorization: Bearer from ~/.balena/token |
| Module | Responsibility |
|---|---|
ExBalena |
Entry: start/0, healthy/0 |
ExBalena.Supervisor |
OTP; Finch pool ExBalena.Finch |
ExBalena.Client |
Tesla client (base URL + auth) |
ExBalena.API.V1 |
Generic GET/POST under /v1 |
ExBalena.API.V2 |
Generic GET/POST under /v2 + light response handling |
Balena |
Domain helpers (current_state/0, current_state/1) |
Balena.Device |
Types (uuid) |
Balena.API |
Shared types (endpoint, body) |
| Requirement | Notes |
|---|---|
| Elixir | Declared ~> 1.0 in mix.exs; this repo’s .tool-versions uses Elixir 1.12.3 / OTP 24.1.2 |
| On-device | io.balena.features.supervisor-api + env vars; agent ≥ endpoint minimum |
| Remote | ~/.balena/token + permission on target device/fleet |
| Runtime deps | tesla ~> 1.4, finch ~> 0.9.1, jason ~> 1.2 |
| Resource | Link |
|---|---|
| Hex | https://hex.pm/packages/ex_balena |
| ExDoc | https://hexdocs.pm/ex_balena |
| Supervisor API | https://docs.balena.io/reference/supervisor/supervisor-api/ |
| Supervisor changelog | https://github.com/balena-os/balena-supervisor/blob/master/CHANGELOG.md |
| Update locks | https://docs.balena.io/learn/deploy/release-strategy/update-locking/ |
| Labels | https://www.balena.io/docs/learn/develop/multicontainer/#labels |
mix deps.get
mix docsmix deps.get
mix test
mix credo
mix dialyzer- PATCH (and other methods) on the client layer
- Named function per documented endpoint, with
@doc since: "<supervisor version>" - Request/response types aligned to official JSON
- First-class remote options (
uuid|deviceId|appId+method+data) on all calls - Streaming helpers for journal / local logs
- Compatibility notes / tests keyed to minimum supervisor versions
Until then, use generic V1 / V2 accessors for routes without named wrappers—and check the device supervisor version before relying on newer fields.
balena® and balenaOS® are trademarks of Balena Ltd. This project is not affiliated with or endorsed by Balena. The Supervisor HTTP API is defined and versioned by Balena; ExBalena is an independent Elixir client.