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

# Datacake

> Client-mode REST API connector for Datacake: read devices, products and current or historical measurements, record measurements, write field and configuration values, trigger downlinks and manage outgoing webhooks from MQTT through the Datacake GraphQL and REST APIs, plus an opt-in webhook receiver

Technical Preview

## Overview

[Datacake](https://datacake.co) is a low-code IoT platform for LoRaWAN, cellular, MQTT and HTTP devices, with dashboards, a rule engine and white-label portals. These connectors connect the Coreflux broker to the Datacake API, so plant logic can read sensor values, push broker data into Datacake devices, change set points and thresholds, and trigger downlinks over MQTT.

* **DatacakeApi** (`DatacakeRestApi.lot`) is a client connector on `https://api.datacake.co` with 23 events: 21 on the [GraphQL API](https://docs.datacake.de/api/graphql-api) and 2 REST endpoints ([record measurements](https://docs.datacake.de/api/record-measurements-via-api) and the product [HTTP ingest URL](https://docs.datacake.de/integrations/webhook)).
* **DatacakeWebhookReceiver** (`cloud-iot_datacake_webhook.lot`, opt-in, server mode) receives Datacake [outgoing webhooks](https://docs.datacake.de/integrations/outgoing-webhooks) and Rule Engine webhook actions, so new measurements, downlinks and alerts reach MQTT without polling.

Datacake publishes no OpenAPI definition. The GraphQL events were checked against the live schema (introspection of `https://api.datacake.co/graphql/`), and every preset query below was validated by the live API.

## Authentication

> [!WARNING]
>
> A personal Datacake token carries every right you have in every workspace you belong to. Prefer an API User token scoped to the workspace and devices this connector needs, keep it in a Hub secret, map it in with `GET SECRET`, and never publish it on a topic other clients can read.

| Token | Where to find it |
| - | - |
| API User token (recommended) | **Members → API Users → Add API User**, then pick workspace permissions and device permissions. |
| Personal token | **Account Settings → API Token**. |

Every event except `IngestProductWebhook` takes `"api_token"` in its MQTT payload and sends `Authorization: Token <api_token>`. Store the token as the secret `DATACAKE_API_TOKEN`.

An invalid token returns `401` with an empty body. A request without a token is treated as anonymous: reads return empty lists or `null`, and writes return `ok: false`.

`IngestProductWebhook` sends no `Authorization` header. The product key in the URL is the credential, so treat it like a secret.

## Payload conventions

* GraphQL events POST `"body"` to `/graphql/` unchanged. The body is `{"query": "...", "variables": {...}}`. The named events are topic presets on the same endpoint, so the query you send decides what comes back. Use the preset queries below as a starting point.
* Validation errors come back as `400` with an `errors` array. Permission problems and unknown ids come back as `200` with `null` data or `ok: false`.
* `RecordMeasurements` takes `"device_id"` (the device UUID shown in the portal URL) and a JSON array body.
* `IngestProductWebhook` takes `"product_key"`, `"path"` (`""` or a custom sub-path ending in `/`, for example `"status/"`) and any JSON body for the product's payload decoder.

## Account and workspaces

| Event | Topic | Notes |
| - | - | - |
| GraphqlQuery | `cloud-iot/datacake/graphql/query` | Any read query from the Datacake GraphQL schema; body \{"query": "...", "variables": \{...}} |
| GraphqlMutation | `cloud-iot/datacake/graphql/mutate` | Any mutation from the schema (266 in Mutations); restrict who may publish here **(write)** |
| GetUser | `cloud-iot/datacake/user/get` | Auth smoke: the token's user; data.user is null when the token is not accepted |
| ListWorkspaces | `cloud-iot/datacake/workspace/list` | Workspaces the token can see (workspace id = inWorkspace for ListDevices) |

**GraphqlMutation** (`cloud-iot/datacake/graphql/mutate`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: SetValueInputType!) { setValue(input: $input) { ok } }",
  "variables": {
    "input": {
      "deviceId": "00000000-0000-0000-0000-000000000000",
      "fieldName": "SETPOINT",
      "value": {
        "valueFloat": 21.5
      }
    }
  }
}
```

**GetUser** (`cloud-iot/datacake/user/get`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query { user { id email fullName isApiuser } }"
}
```

**ListWorkspaces** (`cloud-iot/datacake/workspace/list`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query { allWorkspaces { id name slug } }"
}
```

## Devices

| Event | Topic | Notes |
| - | - | - |
| ListDevices | `cloud-iot/datacake/device/list` | Catalog: devices in a workspace (filters inWorkspace, searchTag, searchName, online) |
| GetDevice | `cloud-iot/datacake/device/get` | One device by id |
| CreateApiDevices | `cloud-iot/datacake/device/create` | Create API (HTTP) devices in a new or existing product; may bill the workspace, needs device-creation rights **(write)** |
| UpdateDevice | `cloud-iot/datacake/device/update` | Rename a device, set tags, location or metadata **(write)** |

**ListDevices** (`cloud-iot/datacake/device/list`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($workspace: String, $tag: String) { allDevices(inWorkspace: $workspace, searchTag: $tag) { id serialNumber verboseName online lastHeard tags product { id name } } }",
  "variables": {
    "workspace": "00000000-0000-0000-0000-000000000000",
    "tag": null
  }
}
```

**GetDevice** (`cloud-iot/datacake/device/get`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($id: String) { device(deviceId: $id) { id serialNumber verboseName online lastHeard location tags metadata product { id name } } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000"
  }
}
```

**CreateApiDevices** (`cloud-iot/datacake/device/create`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: CreateApiDevicesInputType!) { createApiDevices(input: $input) { ok devices { id serialNumber verboseName } } }",
  "variables": {
    "input": {
      "workspace": "00000000-0000-0000-0000-000000000000",
      "plan": "free",
      "productKind": "EXISTING",
      "existingProduct": "00000000-0000-0000-0000-000000000000",
      "devices": [
        {
          "name": "Line 1 sensor",
          "serial": "LINE1-001"
        }
      ]
    }
  }
}
```

**UpdateDevice** (`cloud-iot/datacake/device/update`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($id: String!, $input: UpdateDeviceInputType) { updateDevice(deviceId: $id, input: $input) { ok device { id verboseName tags location } } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000",
    "input": {
      "verboseName": "Line 1 sensor",
      "tags": [
        "porto",
        "line-1"
      ]
    }
  }
}
```

## Measurements

| Event | Topic | Notes |
| - | - | - |
| GetCurrentMeasurements | `cloud-iot/datacake/measurement-current/get` | Latest value of every active field on a device |
| GetDeviceHistory | `cloud-iot/datacake/measurement-history/get` | Historical data; resolution raw or 30s / 20m / 2h / 1d / 1w (raw max 31 days, max 1024 buckets) |

**GetCurrentMeasurements** (`cloud-iot/datacake/measurement-current/get`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($id: String) { device(deviceId: $id) { id verboseName currentMeasurements(allActiveFields: true) { value valueString modified field { fieldName verboseFieldName unit } } } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000"
  }
}
```

**GetDeviceHistory** (`cloud-iot/datacake/measurement-history/get`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($id: String, $fields: [String], $start: String!, $end: String!, $res: String) { device(deviceId: $id) { id history(fields: $fields, timerangestart: $start, timerangeend: $end, resolution: $res) } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000",
    "fields": [
      "TEMPERATURE"
    ],
    "start": "2026-10-05T00:00:00Z",
    "end": "2026-10-06T00:00:00Z",
    "res": "30m"
  }
}
```

## Products

| Event | Topic | Notes |
| - | - | - |
| ListProducts | `cloud-iot/datacake/product/list` | Products (device templates: fields, decoder, dashboard) |
| GetProduct | `cloud-iot/datacake/product/get` | One product with its database fields |

**ListProducts** (`cloud-iot/datacake/product/list`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query { allProducts { id name slug deviceCount } }"
}
```

**GetProduct** (`cloud-iot/datacake/product/get`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($id: String!) { product(id: $id) { id name slug measurementFields { fieldName verboseFieldName fieldType unit } } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000"
  }
}
```

## Values, configuration and downlinks

| Event | Topic | Notes |
| - | - | - |
| SetValue | `cloud-iot/datacake/field-value/set` | Write a value into a device field (manual input / set value widget) **(write)** |
| SetDeviceConfigurationValue | `cloud-iot/datacake/config-value/set` | Override a configuration field (per-device threshold) or resetToDefault **(write)** |
| SendDownlink | `cloud-iot/datacake/downlink/send` | Queue a configured LoRaWAN downlink (downlink id from the product) to a device **(write)** |
| SendApiDownlink | `cloud-iot/datacake/api-downlink/send` | Trigger a configured HTTP downlink on an API device **(write)** |
| SendMqttDownlink | `cloud-iot/datacake/mqtt-downlink/send` | Trigger a configured MQTT downlink on an MQTT device **(write)** |

**SetValue** (`cloud-iot/datacake/field-value/set`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: SetValueInputType!) { setValue(input: $input) { ok } }",
  "variables": {
    "input": {
      "deviceId": "00000000-0000-0000-0000-000000000000",
      "fieldName": "SETPOINT",
      "value": {
        "valueFloat": 21.5
      }
    }
  }
}
```

**SetDeviceConfigurationValue** (`cloud-iot/datacake/config-value/set`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: SetDeviceConfigurationValueInputType!) { setDeviceConfigurationValue(input: $input) { ok } }",
  "variables": {
    "input": {
      "device": "00000000-0000-0000-0000-000000000000",
      "field": "MAX_TEMP",
      "valueNumber": 8.0
    }
  }
}
```

**SendDownlink** (`cloud-iot/datacake/downlink/send`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($device: String!, $downlink: String!) { sendDownlink(device: $device, downlink: $downlink) { ok } }",
  "variables": {
    "device": "00000000-0000-0000-0000-000000000000",
    "downlink": "00000000-0000-0000-0000-000000000000"
  }
}
```

**SendApiDownlink** (`cloud-iot/datacake/api-downlink/send`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($device: String!, $downlink: String!) { sendApiDownlink(device: $device, downlink: $downlink) { ok } }",
  "variables": {
    "device": "00000000-0000-0000-0000-000000000000",
    "downlink": "00000000-0000-0000-0000-000000000000"
  }
}
```

**SendMqttDownlink** (`cloud-iot/datacake/mqtt-downlink/send`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($device: String!, $downlink: String!) { sendMqttDownlink(device: $device, downlink: $downlink) { ok } }",
  "variables": {
    "device": "00000000-0000-0000-0000-000000000000",
    "downlink": "00000000-0000-0000-0000-000000000000"
  }
}
```

## Outgoing webhooks and exports

| Event | Topic | Notes |
| - | - | - |
| CreateWebhook | `cloud-iot/datacake/webhook/create` | Create an outgoing webhook that pushes events to the receiver connector **(write)** |
| UpdateWebhook | `cloud-iot/datacake/webhook/update` | Update or pause an outgoing webhook **(write)** |
| ListWebhookLogs | `cloud-iot/datacake/webhook-log/list` | Recent deliveries of an outgoing webhook |
| CreateManualExport | `cloud-iot/datacake/export/create` | Start a CSV / Excel export of workspace data, delivered by email **(write)** |

**CreateWebhook** (`cloud-iot/datacake/webhook/create`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: CreateWebhookInputType!) { createWebhook(input: $input) { ok webhook { id endpointUrl } } }",
  "variables": {
    "input": {
      "workspace": "00000000-0000-0000-0000-000000000000",
      "endpointUrl": "https://broker.example.com/datacake/webhook/measurements",
      "description": "Coreflux",
      "eventsToSend": [
        "device_measurement"
      ],
      "requestHeaders": [
        {
          "key": "Authorization",
          "value": "Bearer <shared secret>"
        }
      ]
    }
  }
}
```

**UpdateWebhook** (`cloud-iot/datacake/webhook/update`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: UpdateWebhookInputType!) { updateWebhook(input: $input) { ok webhook { id endpointUrl } } }",
  "variables": {
    "input": {
      "workspace": "00000000-0000-0000-0000-000000000000",
      "id": "00000000-0000-0000-0000-000000000000",
      "description": "Coreflux (line 1)"
    }
  }
}
```

**ListWebhookLogs** (`cloud-iot/datacake/webhook-log/list`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "query($id: String!) { webhookLogs(webhookId: $id) { datetime request } }",
  "variables": {
    "id": "00000000-0000-0000-0000-000000000000"
  }
}
```

**CreateManualExport** (`cloud-iot/datacake/export/create`)

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "query": "mutation($input: CreateManualExportInputType!) { createManualExport(input: $input) { ok errors } }",
  "variables": {
    "input": {
      "workspaceId": "00000000-0000-0000-0000-000000000000",
      "name": "Line 1 week",
      "exportFrom": "2026-09-28T00:00:00Z",
      "exportUntil": "2026-10-05T00:00:00Z",
      "emailReceivers": [
        "ops@example.com"
      ]
    }
  }
}
```

## REST events

| Event | Request | Topic | Notes |
| - | - | - | - |
| RecordMeasurements | `POST /v1/devices/{device_id}/record/?batch=true` | `cloud-iot/datacake/measurement/record` | Catalog: record field values into a device; body \[\{"field": "TEMPERATURE", "value": 23.5, "timestamp": 1759708800}] (timestamp optional; 1 write per second per field) **(write)** |
| IngestProductWebhook | `POST /integrations/api/{product_key}/{path}` | `cloud-iot/datacake/ingest/send` | Unauthenticated product HTTP URL (API devices): any JSON body, the product's payload decoder connectors it by serial; path "" or a custom sub-path like "status/" **(write)** |

Datacake rate-limits recorded measurements to 1 write per second per field. Add an optional Unix `timestamp` to each item to backfill.

## Webhook receiver

`DatacakeWebhookReceiver` listens on port `9005` and publishes each delivery on MQTT unchanged:

| Path | Datacake source | MQTT topic |
| - | - | - |
| `/datacake/webhook/measurements` | Outgoing webhook, event *Device measurements* (`device_measurement`) | `cloud-iot/datacake/webhook-measurement/received` |
| `/datacake/webhook/decoder-output` | Outgoing webhook, event *Decoder output* (`decoder_output`) | `cloud-iot/datacake/webhook-decoder-output/received` |
| `/datacake/webhook/downlinks` | Outgoing webhook, event *Downlinks* (`downlink`) | `cloud-iot/datacake/webhook-downlink/received` |
| `/datacake/webhook/rules` | Rule Engine webhook action | `cloud-iot/datacake/webhook-rule/received` |

1. In Datacake, open **Integrations → Webhooks → Add Webhook** (or publish on `cloud-iot/datacake/webhook/create`), use one webhook per event type, and point it at a public HTTPS URL.
2. Add a request header with a shared secret, for example `Authorization: Bearer <secret>`. Datacake recommends this over IP allow lists because its infrastructure scales dynamically.
3. Put a reverse proxy or tunnel with a public TLS certificate in front of port `9005`, have it check the header, and forward to `http://<broker>:9005/datacake/webhook/...`. The connector itself does not check headers.

> [!WARNING]
>
> Deploying the receiver opens an HTTP port on the broker. Deploy it only where you need push delivery.

## Examples

Read the latest values of one device:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_token": "<DATACAKE_API_TOKEN>",
  "body": {
    "query": "query($id: String) { device(deviceId: $id) { id verboseName currentMeasurements(allActiveFields: true) { value valueString modified field { fieldName verboseFieldName unit } } } }",
    "variables": {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  }
}
```

Record a plant reading into a Datacake device:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "api_token": "<DATACAKE_API_TOKEN>",
  "device_id": "<device uuid>",
  "body": [
    {
      "field": "TEMPERATURE",
      "value": 23.5
    }
  ]
}
```

Helper actions (poll a device every minute, forward a plant topic into Datacake, fan webhook events out to plant topics):

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DatacakePollCurrentMeasurements
ON EVERY 1 MINUTE DO
    SET "token" WITH GET SECRET "DATACAKE_API_TOKEN"
    SET "dev" WITH GET SECRET "DATACAKE_DEVICE_ID"
    PUBLISH TOPIC "cloud-iot/datacake/measurement-current/get" WITH { "api_token": {token}, "body": { "query": "query($id: String) { device(deviceId: $id) { id verboseName currentMeasurements(allActiveFields: true) { value valueString modified field { fieldName verboseFieldName unit } } } }", "variables": { "id": {dev} } } }

DEFINE ACTION DatacakeRecordLineTemperature
ON TOPIC "porto/line1/temperature" DO
    SET "token" WITH GET SECRET "DATACAKE_API_TOKEN"
    SET "dev" WITH GET SECRET "DATACAKE_DEVICE_ID"
    PUBLISH TOPIC "cloud-iot/datacake/measurement/record" WITH { "api_token": {token}, "device_id": {dev}, "body": [ { "field": "TEMPERATURE", "value": PAYLOAD } ] }

DEFINE ACTION DatacakeWebhookToPlant
ON TOPIC "cloud-iot/datacake/webhook-measurement/received" DO
    SET "kind" WITH (GET JSON "event" IN PAYLOAD AS STRING)
    PUBLISH TOPIC "plant/datacake/" + {kind} WITH PAYLOAD
```

## Writes

Write events: GraphqlMutation, CreateApiDevices, UpdateDevice, SetValue, SetDeviceConfigurationValue, SendDownlink, SendApiDownlink, SendMqttDownlink, CreateWebhook, UpdateWebhook, CreateManualExport, RecordMeasurements, IngestProductWebhook. `GraphqlMutation` can run any of the 266 mutations in the schema, including deletes, so restrict who may publish on `cloud-iot/datacake/graphql/mutate` with a broker rule. `CreateApiDevices` can add billed devices to the workspace. No preset deletes anything.

## Not packaged

Account, billing, white-label, SSO, Node-RED (CakeRed) and organization mutations; product decoder and dashboard editing; LoRaWAN network server and gateway provisioning; rule creation; every delete. They stay reachable through `GraphqlMutation` if you need them. Datacake's own MQTT broker (`mqtt.datacake.co`) is better reached with a Coreflux MQTT bridge connector than over REST.

## Status

Technical preview, blocked on the authenticated live test. Every event reaches `api.datacake.co`, an invalid token returns `401` on every token event, every preset query validates against the live schema, and both REST paths answer as documented for unknown ids. A real token, workspace and test device are still needed for the GET and POST round trip.


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