> ## 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.

# Golioth

> Client-mode REST API connector for the Golioth Management API: manage devices, read and write LightDB State, query LightDB Stream, call device RPCs, change settings and run OTA releases and cohort deployments from MQTT, plus an opt-in pipeline webhook receiver

Technical Preview

## Overview

[Golioth](https://golioth.io) is an IoT device management cloud for microcontroller fleets (Zephyr, nRF Connect SDK, ESP-IDF and ModusToolbox through the Golioth Firmware SDK). Devices talk to Golioth over CoAP, and the [Management API](https://docs.golioth.io/reference/management-api/) exposes the same project to applications. These connectors let the Coreflux broker read device state and time series from Golioth, push desired state, settings and RPC calls back to devices, and drive OTA updates from MQTT.

* **GoliothManagementApi** (`GoliothRestApi.lot`) is a client connector on `https://api.golioth.io` with 45 events.
* **GoliothWebhookReceiver** (`cloud-iot_golioth_webhook.lot`, opt-in, server mode) receives data that a Golioth Pipeline sends with the `webhook` destination, so device stream data reaches MQTT without polling.

The events follow the Management API [OpenAPI definition](https://api.golioth.io/openapi.json).

## Authentication

> [!WARNING]
>
> Golioth API keys are project scoped and can run Management API operations in that project. Keep the key in a Hub secret, map it in with `GET SECRET`, and never publish it on a topic other clients can read.

1. In the Golioth console, open your project and create an API key under **API Keys** (or run `goliothctl apikeys create`).
2. Store it as the secret `GOLIOTH_API_KEY`. Every event takes `"api_key"` in its MQTT payload and sends it in the `x-api-key` header.
3. Every event also takes `"project_id"`. The OTA cohort, package, deployment, location and pipeline events use organization-scoped paths and take `"organization_id"` too.

A missing or wrong key returns `401` with `{"code": 7, "message": "failed to check permissions to access/modify this resource: user not allowed to call method"}`.

## Payload conventions

* Path ids (`device_id`, `tag_id`, `setting_id`, `blueprint_id`, `artifact_id`, `release_id`, `cohort_id`, `deployment_id`, `pipeline_id`) go at the top level of the MQTT payload.
* LightDB paths go in `"path"` without a leading slash, for example `env/temp`.
* Events marked `?{query}` take the query string already URL-encoded, for example `page=0&perPage=100`. Send `"query": ""` when there is nothing to add.
* Request bodies come from `"body"` and are sent as JSON.

## Devices

| Event | Request | Topic | Notes |
| - | - | - | - |
| GetProject | `GET /v1/projects/{project_id}` | `cloud-iot/golioth/project/get` | Auth smoke: the project behind the API key (API keys are project scoped) |
| ListDevices | `GET /v1/projects/{project_id}/devices?{query}` | `cloud-iot/golioth/device/list` | Devices in the project (paged, max 100 per page); query page=0\&perPage=100 \| tags=\<tagId> \| term=press \| cohortId=... \| statePath=env (send "" for none) |
| GetDevice | `GET /v1/projects/{project_id}/devices/{device_id}` | `cloud-iot/golioth/device/get` | One device: name, hardware ids, tag ids, blueprint, cohort, enabled, status, lastReport, metadata |
| CreateDevice | `POST /v1/projects/{project_id}/devices` | `cloud-iot/golioth/device/create` | **Write.** Create a device; body \{"name": "press-01", "tagIds": \["..."], "blueprintId": "...", "cohortId": "..."} (credentials are not packaged) |
| UpdateDevice | `PATCH /v1/projects/{project_id}/devices/{device_id}` | `cloud-iot/golioth/device/update` | **Write.** Partial device update; body \{"name": "press-01", "addTagId": \["..."], "cohortId": "...", "enabled": true}; cohortId "" leaves the cohort |
| GetDeviceNetwork | `GET /v1/projects/{project_id}/devices/{device_id}/network` | `cloud-iot/golioth/device-network/get` | Network the device belongs to (network id, device count, last seen) |

## LightDB State, LightDB Stream and RPC

LightDB State is the device twin: devices observe paths such as `config/temp` and react when you write them. LightDB Stream holds the time series that devices send to `/.s/` paths.

| Event | Request | Topic | Notes |
| - | - | - | - |
| GetDeviceState | `GET /v1/projects/{project_id}/devices/{device_id}/data/{path}` | `cloud-iot/golioth/state/get` | Catalog: LightDB State at a path, e.g. path env/temp or config (no leading slash) |
| SetDeviceState | `POST /v1/projects/{project_id}/devices/{device_id}/data/{path}` | `cloud-iot/golioth/state/set` | **Write.** Catalog: write desired state at a path the device observes; body \{"min": 20.0, "max": 40.0} or a single value (true, 42, "on") |
| SetStateByTag | `POST /v1/projects/{project_id}/devicesByTag/{tag_id}/data/{path}` | `cloud-iot/golioth/state-by-tag/set` | **Write.** Bulk: write the same LightDB State path on every device with a tag; body as SetDeviceState |
| GetDeviceStream | `GET /v1/projects/{project_id}/devices/{device_id}/stream?{query}` | `cloud-iot/golioth/stream/get` | Time-series points of one device; query start=2026-10-06T00:00:00Z\&end=2026-10-06T12:00:00Z\&perPage=100 \| interval=1h |
| QueryDeviceStream | `POST /v1/projects/{project_id}/devices/{device_id}/stream` | `cloud-iot/golioth/stream/query` | Read (POST): LightDB Stream query for one device; body \{"start": "...Z", "end": "...Z", "query": \{"fields": \[\{"path": "time"}, \{"path": "env.temp", "agg": "avg", "type": "float"}], "timeBucket": "15m"}} |
| QueryProjectStream | `POST /v1/projects/{project_id}/stream` | `cloud-iot/golioth/project-stream/query` | Read (POST): LightDB Stream query across devices; body as QueryDeviceStream plus "deviceId": \[...] or "tags": \[...] |
| CallDeviceRpc | `POST /v1/projects/{project_id}/devices/{device_id}/rpc` | `cloud-iot/golioth/rpc/call` | **Write.** Call a method the firmware registered; body \{"method": "set\_speed", "params": \[1200], "timeout": 10}; device must be online |

## Settings, tags and blueprints

| Event | Request | Topic | Notes |
| - | - | - | - |
| ListSettings | `GET /v1/projects/{project_id}/settings?{query}` | `cloud-iot/golioth/setting/list` | Project, blueprint or device settings; query deviceId=... \| blueprintId=... (send "" for project level) |
| GetDeviceSettings | `GET /v1/projects/{project_id}/devices/{device_id}/settings` | `cloud-iot/golioth/device-setting/get` | Effective settings of one device (project, blueprint and device overrides) |
| CreateSetting | `POST /v1/projects/{project_id}/settings` | `cloud-iot/golioth/setting/create` | **Write.** Create a setting; body \{"key": "LOOP\_DELAY\_S", "dataType": "integer", "value": 60} (+ "deviceId" or "blueprintId" for an override) |
| UpdateSetting | `PATCH /v1/projects/{project_id}/settings/{setting_id}` | `cloud-iot/golioth/setting/update` | **Write.** Change a setting value; body \{"value": 30}; devices pick it up through the Settings service |
| ListTags | `GET /v1/projects/{project_id}/tags?{query}` | `cloud-iot/golioth/tag/list` | Device tags; query page=0\&perPage=100 \| tagName=line-1 |
| CreateTag | `POST /v1/projects/{project_id}/tags` | `cloud-iot/golioth/tag/create` | **Write.** Create a device tag; body \{"name": "line-1"} |
| ListBlueprints | `GET /v1/projects/{project_id}/blueprints?{query}` | `cloud-iot/golioth/blueprint/list` | Blueprints (hardware / firmware families); query page=0\&perPage=100 \| blueprintName=... |
| GetBlueprint | `GET /v1/projects/{project_id}/blueprints/{blueprint_id}` | `cloud-iot/golioth/blueprint/get` | One blueprint: name, board, platform |

## OTA updates

Golioth has two OTA models in the same API. **Artifacts and releases** target devices by tag and blueprint. **Packages, cohorts and deployments** group devices into cohorts, and a new deployment starts updating every device in the cohort right away.

| Event | Request | Topic | Notes |
| - | - | - | - |
| ListArtifacts | `GET /v1/projects/{project_id}/artifacts?{query}` | `cloud-iot/golioth/artifact/list` | Uploaded firmware / file artifacts; query package=main\&perPage=50 \| version=1.2.0 \| blueprintId=... |
| GetArtifact | `GET /v1/projects/{project_id}/artifacts/{artifact_id}` | `cloud-iot/golioth/artifact/get` | One artifact: package, version, size, hash, blueprint |
| CreateArtifact | `POST /v1/artifacts` | `cloud-iot/golioth/artifact/create` | **Write.** Upload an artifact; body \{"projectId": "...", "package": "main", "version": "1.2.0", "content": "\<base64 binary>"} (mind MQTT payload size) |
| ListReleases | `GET /v1/projects/{project_id}/releases?{query}` | `cloud-iot/golioth/release/list` | OTA releases; query rollout=true \| deviceTags=\<tagId> \| perPage=50 (send "" for none) |
| GetRelease | `GET /v1/projects/{project_id}/releases/{release_id}` | `cloud-iot/golioth/release/get` | One release: artifacts, device tags, release tags, rollout flag |
| CreateRelease | `POST /v1/projects/{project_id}/releases` | `cloud-iot/golioth/release/create` | **Write.** Create a release from artifacts; body \{"artifactIds": \["..."], "deviceTagIds": \["..."], "releaseTags": \["v1.2.0"], "rollout": false} |
| UpdateRelease | `PATCH /v1/projects/{project_id}/releases/{release_id}` | `cloud-iot/golioth/release/update` | **Write.** Start or stop a rollout, retarget tags; body \{"rollout": true} \| \{"addDeviceTagId": \["..."]} |
| ListPackages | `GET /v1/organizations/{organization_id}/projects/{project_id}/packages?{query}` | `cloud-iot/golioth/package/list` | OTA packages (upgradeable components: main firmware, modem, ML model); query page=0\&perPage=100 |
| CreatePackage | `POST /v1/organizations/{organization_id}/projects/{project_id}/packages` | `cloud-iot/golioth/package/create` | **Write.** Create a package; body \{"packageId": "main", "description": "Main firmware", "metadata": \{"board": "nrf9160dk"}} |
| ListCohorts | `GET /v1/organizations/{organization_id}/projects/{project_id}/cohorts?{query}` | `cloud-iot/golioth/cohort/list` | Cohorts (device groups that share a deployment); query page=0\&perPage=100 |
| GetCohort | `GET /v1/organizations/{organization_id}/projects/{project_id}/cohorts/{cohort_id}` | `cloud-iot/golioth/cohort/get` | One cohort and its active deployment |
| CreateCohort | `POST /v1/organizations/{organization_id}/projects/{project_id}/cohorts` | `cloud-iot/golioth/cohort/create` | **Write.** Create a cohort; body \{"name": "porto-line-1"}; add devices with UpdateDevice cohortId |
| ListDeployments | `GET /v1/organizations/{organization_id}/projects/{project_id}/cohorts/{cohort_id}/deployments?{query}` | `cloud-iot/golioth/deployment/list` | Deployments of a cohort; query page=0\&perPage=50 \| name=... |
| GetDeployment | `GET /v1/organizations/{organization_id}/projects/{project_id}/cohorts/{cohort_id}/deployments/{deployment_id}` | `cloud-iot/golioth/deployment/get` | One deployment: artifacts and creation time |
| CreateDeployment | `POST /v1/organizations/{organization_id}/projects/{project_id}/cohorts/{cohort_id}/deployments` | `cloud-iot/golioth/deployment/create` | **Write.** Deploy artifacts to every device in the cohort (devices start updating immediately); body \{"name": "1.2.0", "artifactIds": \["..."]} |
| ListOtaEvents | `GET /v1/organizations/{organization_id}/projects/{project_id}/ota-events?{query}` | `cloud-iot/golioth/ota-event/list` | OTA progress events from devices; query cohortId=...\&deviceId=...\&startTime=...Z\&limit=100 \| pageToken=... |
| GetOtaEventStats | `GET /v1/organizations/{organization_id}/projects/{project_id}/ota-events/stats?{query}` | `cloud-iot/golioth/ota-event-stats/get` | OTA outcome counts; query cohortId=...\&startTime=...Z (send "" for none) |

## Logs, locations and pipelines

| Event | Request | Topic | Notes |
| - | - | - | - |
| ListDeviceLocations | `GET /v1/organizations/{organization_id}/projects/{project_id}/location?{query}` | `cloud-iot/golioth/location/list` | Latest device locations (Location service); query deviceIds=\<id> (repeat for several, send "" for all) |
| ListLogs | `GET /v1/projects/{project_id}/logs?{query}` | `cloud-iot/golioth/log/list` | Device logs sent with the Golioth logging service; query deviceId=...\&level=error\&start=...Z\&end=...Z \| module=app |
| ListDeviceActivityLogs | `GET /v1/projects/{project_id}/device-activity-logs?{query}` | `cloud-iot/golioth/activity-log/list` | Latest device activity (connects, disconnects, errors); query page=0\&perPage=100 |
| ListFirmwareLogs | `GET /v1/projects/{project_id}/firmware/logs?{query}` | `cloud-iot/golioth/firmware-log/list` | Firmware update state reports; query deviceId=...\&state=... \| version=1.2.0\&target=main |
| ListPipelines | `GET /v1/organizations/{organization_id}/projects/{project_id}/pipelines` | `cloud-iot/golioth/pipeline/list` | Data routing pipelines (filter, transformers, destinations such as webhook) |
| GetPipeline | `GET /v1/organizations/{organization_id}/projects/{project_id}/pipelines/{pipeline_id}` | `cloud-iot/golioth/pipeline/get` | One pipeline; the YAML document is returned base64 encoded |
| CreatePipeline | `POST /v1/organizations/{organization_id}/projects/{project_id}/pipelines` | `cloud-iot/golioth/pipeline/create` | **Write.** Create a pipeline, e.g. the webhook to cloud-iot\_golioth\_webhook.lot; body \{"name": "to-coreflux", "enabled": true, "pipeline": "\<base64 YAML>"} (usage billed) |

## Webhook receiver

Deploy `cloud-iot_golioth_webhook.lot` only if you want Golioth to push data to the broker. It opens HTTP port `9003`. Put a reverse proxy or tunnel with a public TLS certificate in front of it and have it check a shared header, because the connector itself doesn't.

Golioth delivers data through [Pipelines](https://docs.golioth.io/data-routing), which replace the older Output Streams. Create a pipeline in the console (or with `CreatePipeline`, base64-encoding the YAML) that sends data to the [webhook destination](https://docs.golioth.io/data-routing/destinations/webhook):

```yaml theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
filter:
  path: "*"
  content_type: application/cbor
steps:
  - name: to-json
    transformer:
      type: cbor-to-json
      version: v1
  - name: to-coreflux
    transformer:
      type: inject-metadata
      version: v1
    destination:
      type: webhook
      version: v1
      parameters:
        url: https://<public host>/golioth/webhook/stream
        headers:
          x-coreflux-secret: $COREFLUX_WEBHOOK_SECRET
```

Store `COREFLUX_WEBHOOK_SECRET` as a pipeline secret in the Golioth project. The `inject-metadata` step puts `device_id`, `project_id` and `timestamp` in the body, because the receiver only forwards the body to MQTT. Deliveries are published on `cloud-iot/golioth/webhook-stream/received`. Other pipelines can post to `/golioth/webhook/events`, which publishes on `cloud-iot/golioth/webhook-event/received`. Webhook deliveries count as Golioth usage after the free tier.

## Examples

Write alert limits that the device observes at `config/temp`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"api_key": "<GOLIOTH_API_KEY>", "project_id": "coreflux-porto", "device_id": "<device id>", "path": "config/temp", "body": {"min": 20.0, "max": 40.0}}
```

Get the 15-minute average temperature of one device:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"api_key": "<GOLIOTH_API_KEY>", "project_id": "coreflux-porto", "device_id": "<device id>", "body": {"start": "2026-10-06T00:00:00Z", "end": "2026-10-06T12:00:00Z", "query": {"fields": [{"path": "time"}, {"path": "env.temp", "alias": "temperature", "agg": "avg", "type": "float"}], "timeBucket": "15m"}}}
```

Call a firmware RPC:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"api_key": "<GOLIOTH_API_KEY>", "project_id": "coreflux-porto", "device_id": "<device id>", "body": {"method": "set_speed", "params": [1200], "timeout": 10}}
```

Deploy firmware to a cohort:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"api_key": "<GOLIOTH_API_KEY>", "organization_id": "<org id>", "project_id": "coreflux-porto", "cohort_id": "<cohort id>", "body": {"name": "1.2.0", "artifactIds": ["<artifact id>"]}}
```

LoT actions that wire these events to plant topics:

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION GoliothSetTempLimits
ON TOPIC "porto/line-1/temp-limits/set" DO
    SET "key" WITH GET SECRET "GOLIOTH_API_KEY"
    SET "proj" WITH GET SECRET "GOLIOTH_PROJECT_ID"
    SET "dev" WITH GET SECRET "GOLIOTH_DEVICE_ID"
    SET "lo" WITH GET JSON "min" IN PAYLOAD AS STRING
    SET "hi" WITH GET JSON "max" IN PAYLOAD AS STRING
    PUBLISH TOPIC "cloud-iot/golioth/state/set" WITH { "api_key": {key}, "project_id": {proj}, "device_id": {dev}, "path": "config/temp", "body": { "min": {lo}, "max": {hi} } }

DEFINE ACTION GoliothPollDevices
ON EVERY 1 MINUTE DO
    SET "key" WITH GET SECRET "GOLIOTH_API_KEY"
    SET "proj" WITH GET SECRET "GOLIOTH_PROJECT_ID"
    PUBLISH TOPIC "cloud-iot/golioth/device/list" WITH { "api_key": {key}, "project_id": {proj}, "query": "perPage=100&statePath=env" }

DEFINE ACTION GoliothWebhookToPlant
ON TOPIC "cloud-iot/golioth/webhook-stream/received" DO
    SET "dev" WITH (GET JSON "device_id" IN PAYLOAD AS STRING)
    SET "data" WITH (GET JSON "data" IN PAYLOAD AS STRING)
    PUBLISH TOPIC "plant/golioth/" + {dev} + "/stream" WITH {data}
```

## Writes

15 events change data in Golioth or on devices, and each one is marked **Write** above. `CreateDeployment`, `UpdateRelease` with `"rollout": true`, `SetStateByTag` and `CallDeviceRpc` act on devices in the field, so test them on a lab cohort or tag first. No `DELETE` operation is packaged.

## Not packaged

Device credentials (PSKs and certificates), provisioning, API keys, users and access policies, PKI, pipeline secrets, billing and usage, and every `DELETE` are left out of this template.

## Status

Technical preview, and blocked on a live test. The events were checked against Golioth's published Management API OpenAPI, and all 45 events reach the real `api.golioth.io` host and are rejected without a valid API key as expected. An authenticated read and write with a real Golioth project and API key is still pending.


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