Demand Response Provider Integration
This guide is for flexibility providers and aggregators integrating their own dispatch platform with Fentrica: exporting the metadata and live telemetry of a building's controllable equipment, then dispatching Demand Response and aFRR setpoints to it.
Everything here is machine-to-machine over HTTPS with an API key — no human login, no WebSocket, no device-side deployment. Commands travel through the Fentrica Cloud Broker to the edge device and the device's reply comes back in the same HTTP response.
The commands in Step 5 change the setpoint of live building equipment and override local optimisation, including peak shaving and cloud schedules. Read Precedence and safety before you dispatch against a production site.
What you will build
One-time setup, then a continuous dispatch loop
Requirements
- An API key with Edge devices → read & write, covering the site you will control. Read-only is not offered for edge devices, so a read-only key cannot even export telemetry. See API Keys.
- Your organization id (
orgId) and the target device id (deviceId) — the controller the equipment is wired to. - The technical connection id (
connectionId) whose datapoints you will read and steer. See Step 2. - The building's controllable equipment already commissioned as a technical system with datapoints of access type
READ_WRITEorWRITE. AREADdatapoint can be exported and polled but never dispatched.
Architecture overview
Overrides live on the edge, not in the cloud
A dispatched setpoint is held in the edge device's memory and persisted locally, not written to a cloud database. It therefore survives a controller restart and keeps acting even if the site loses its uplink mid-window. It is bounded by the window you send, so a lost connection can never leave equipment pinned indefinitely.
The request envelope
Every command below uses the shared edge-device endpoint, with the command name in the path:
POST https://broker.fentrica.com/api/orgs/{orgId}/larva-device-admin/{deviceId}/cmd/{command}
| Header | Value |
|---|---|
X-Api-Key | your key, lrv_… |
Content-Type | application/json |
Request body — data is the command payload, reqId is an optional UUID echoed back for log correlation:
{
"data": {},
"reqId": "d3b07384-d9a0-4c9b-8f1e-2f4a6c8e0b12"
}
A successful response is the device's own reply:
{
"reqId": "d3b07384-d9a0-4c9b-8f1e-2f4a6c8e0b12",
"data": {}
}
See Edge Device REST API for the full envelope reference.
Step 1 — Create a scoped API key
Follow API Keys and grant Edge devices → read & write, scoped to the specific sites you are contracted for rather than the whole organization. Copy the lrv_… secret at creation — it is shown once.
Use a separate key per flexibility program or per market. Revocation is immediate and permanent, so a per-program key lets you cut one integration without interrupting the others, and makes the last used column meaningful when you audit.
Step 2 — Resolve the connection id
Every command in this guide is scoped to one technical connection — the device's link to a protocol endpoint (a Modbus TCP inverter, an MQTT broker, an Obix gateway), which owns a set of datapoints.
Fentrica issues the orgId, deviceId and connectionId values for your contracted sites as part of partner onboarding — your integration starts from a ready-made inventory of controllable assets, with no discovery phase to build. Adding a site or a new flexibility program? Contact [email protected] and we will provision the ids for it.
Treat connectionId and datapointId as opaque, stable identifiers. Do not derive them, and do not assume a format.
Step 3 — Export datapoint metadata
getTechnicalDataPoints returns the configuration of every datapoint on a connection. Call it at onboarding and whenever you re-sync — not in your dispatch loop.
curl -X POST \
'https://broker.fentrica.com/api/orgs/{orgId}/larva-device-admin/{deviceId}/cmd/getTechnicalDataPoints' \
-H 'X-Api-Key: lrv_…' \
-H 'Content-Type: application/json' \
-d '{ "data": { "connectionId": "<connectionId>", "limit": 50, "offset": 0 } }'
Response — count is the total on the connection, so page with limit/offset until you have them all:
{
"reqId": "8f14e45f-ceea-467a-9b0f-1c2d3e4f5a6b",
"data": {
"count": 2,
"datapoints": [
{
"id": "c1f9a2e4-5b6d-4e7f-8a90-1b2c3d4e5f60",
"connectionId": "b759c6a5-95be-4ef0-9f7d-3e75eed8f248",
"name": "Battery power setpoint",
"accessType": "READ_WRITE",
"writeInputType": "number",
"writeInputData": { "min": -50, "max": 50 },
"analyticsInterval": 900,
"analyticsCalculateConsumption": false,
"broadcastInterval": 60,
"writeInterval": 0,
"cloudScheduleId": null,
"scaler": { "inputValue1": 0, "outputValue1": 0, "inputValue2": 100, "outputValue2": 100 },
"alarmData": null,
"data": { "register": 40001, "type": "int16" },
"createdAt": "2026-03-11T08:22:14.000Z",
"updatedAt": "2026-07-02T11:05:41.000Z"
},
{
"id": "d2e0b3f5-6c7e-4f80-9ba1-2c3d4e5f6071",
"connectionId": "b759c6a5-95be-4ef0-9f7d-3e75eed8f248",
"name": "Grid import power",
"accessType": "READ",
"analyticsInterval": 900,
"broadcastInterval": 60,
"writeInterval": 0,
"cloudScheduleId": null,
"scaler": { "inputValue1": 0, "outputValue1": 0, "inputValue2": 1000, "outputValue2": 1000 },
"alarmData": null,
"data": { "register": 40010, "type": "int32" },
"createdAt": "2026-03-11T08:22:14.000Z",
"updatedAt": "2026-03-11T08:22:14.000Z"
}
]
}
}
The fields that matter for a dispatch integration:
| Field | Why it matters |
|---|---|
id | The datapointId you dispatch against. |
accessType | READ_WRITE / WRITE are dispatchable; READ is telemetry only. |
writeInputData.min / .max | The commissioned operating envelope. Clamp your setpoints to it — Fentrica does not clamp for you. |
scaler | Maps engineering units to the raw protocol value. You always send engineering units; the device applies the scaler. |
analyticsInterval | How often this datapoint is archived to the cloud, in seconds. 0 means it is not archived. |
broadcastInterval | How often the device pushes this value out unprompted, in seconds. |
cloudScheduleId | Non-null means a cloud schedule also drives this datapoint — your dispatch will outrank it while your window is open. |
Step 4 — Poll live telemetry
getTechnicalConnectionValues returns the connection's live status plus the current value of every datapoint in one call. This is the request to poll; it is served entirely from the device's memory and touches no database.
curl -X POST \
'https://broker.fentrica.com/api/orgs/{orgId}/larva-device-admin/{deviceId}/cmd/getTechnicalConnectionValues' \
-H 'X-Api-Key: lrv_…' \
-H 'Content-Type: application/json' \
-d '{ "data": { "id": "<connectionId>" } }'
{
"reqId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
"data": {
"connectionId": "b759c6a5-95be-4ef0-9f7d-3e75eed8f248",
"status": 1,
"error": null,
"timestamp": "2026-07-27T14:32:05.118Z",
"values": {
"c1f9a2e4-5b6d-4e7f-8a90-1b2c3d4e5f60": {
"value": -12.5,
"timestamp": "2026-07-27T14:32:04.902Z",
"locked": true,
"lockedBy": "demand-response"
},
"d2e0b3f5-6c7e-4f80-9ba1-2c3d4e5f6071": {
"value": 41.2,
"timestamp": "2026-07-27T14:32:04.902Z"
}
}
}
}
status is the connection's live state:
status | Meaning |
|---|---|
1 | Connected — values are live. |
2 | Connecting. |
0 | Disconnected — values are the last known readings. |
-1 | Error — error carries the message; values are the last known readings. |
Absent means default
Each entry always carries value and timestamp, and omits every field that is at its default. Treat a missing locked, deviationActivated or error as false/null. This roughly halves the payload, which matters when you poll a large connection over a metered uplink.
Per-datapoint fields beyond value and timestamp:
| Field | Meaning when present |
|---|---|
error | The value could not be read; value is null. One bad datapoint never fails the whole response. |
locked / lockedBy | Something holds the datapoint. "demand-response" is your own override; "peak-shaving" or "deviation" is local optimisation. |
deviationActivated | A cloud schedule deviation is currently applied. |
deviationCachedValue | The value held before that deviation. A number, or a string when the reading exceeds 2⁵³ (64-bit meter registers) so no precision is lost — accept both. |
The device answers from memory, so this endpoint is designed to be polled continuously — your dispatch loop works from genuinely live readings rather than a cached snapshot. For slow-moving equipment a 10–60 second cadence is usually plenty; save the fastest polling for active dispatch windows.
This endpoint is a live snapshot for control decisions. For billing, settlement or baselining use the archived measurement history, which is written on each datapoint's analyticsInterval — ask Fentrica for the metering export appropriate to your program.
Step 5 — Dispatch a Demand Response setpoint
demandResponseOverride places a bounded override on a single datapoint. The device applies it immediately if the window has already opened, and holds it until the window ends.
curl -X POST \
'https://broker.fentrica.com/api/orgs/{orgId}/larva-device-admin/{deviceId}/cmd/demandResponseOverride' \
-H 'X-Api-Key: lrv_…' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"datapointId": "c1f9a2e4-5b6d-4e7f-8a90-1b2c3d4e5f60",
"value": -25,
"from": "2026-07-27T14:30:00.000Z",
"to": "2026-07-27T15:00:00.000Z"
}
}'
The reply carries the override id — keep it, it is the only way to release the window early:
{
"reqId": "3c59dc04-8e88-4504-9b0c-2f5a1d3e7b91",
"data": { "id": "7e1a4b2c-9d3f-4a58-b6c7-08e9f0a1b2c3" }
}
| Field | Rules |
|---|---|
datapointId | Must exist on a connection loaded by this device, and be READ_WRITE or WRITE. |
value | A finite number in engineering units. The device applies the datapoint's scaler. Clamp to writeInputData.min/max yourself. |
from | ISO 8601. May be in the past (applies at once) or the future (queued). |
to | ISO 8601, after from and in the future. |
Windows are bounded to 24 hours. A longer window is rejected, so a malformed dispatch cannot pin equipment indefinitely.
You may hold several windows on one datapoint — queue tomorrow's event now. The window covering the current moment wins, and windows that have ended are discarded rather than retained.
Step 6 — Release a window early
Markets recall availability. demandResponseCancel drops the named window and hands the datapoint straight back to whatever was driving it before — a cloud schedule resumes at once rather than waiting for its next cycle.
curl -X POST \
'https://broker.fentrica.com/api/orgs/{orgId}/larva-device-admin/{deviceId}/cmd/demandResponseCancel' \
-H 'X-Api-Key: lrv_…' \
-H 'Content-Type: application/json' \
-d '{ "data": { "id": "7e1a4b2c-9d3f-4a58-b6c7-08e9f0a1b2c3" } }'
{
"reqId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
"data": { "success": true }
}
Cancel is idempotent: cancelling an id that has already ended or been cancelled still returns success: true, because a window that no longer exists is exactly the state you asked for. You do not need to track whether a window has expired before releasing it.
Precedence and safety
A Demand Response override is the highest-priority writer on a datapoint:
Highest priority first
- While your window is open, peak shaving stops controlling that datapoint and cloud schedule deviations stand aside. Both resume when the window ends or is cancelled.
- Your override does not delete or alter the site's cloud schedules. They are untouched and simply outranked for the duration.
- Because DR outranks demand-charge protection, a long high-import dispatch can expose the site to a demand peak that peak shaving would otherwise have prevented. That trade-off is deliberate — grid obligations win — so keep it in mind when sizing windows.
- The override is bounded by
toand persisted locally with that boundary, so a controller restart mid-window resumes it correctly and a restart after it has passed does not resurrect a stale setpoint.
writeInputData.min/max describe what the equipment was commissioned to accept. Fentrica does not clamp your value to it. Sending a setpoint outside that envelope is passed through to the equipment.
Audit trail
Every apply and release is logged on the device and reported to the cloud under report code 4005 — OPTIMIZATION_DEMAND_RESPONSE ("Demand response adjustment", optimization group, INFO).
The cloud stores each entry against the technical system the datapoint belongs to, so DR activity appears in the site's log alongside peak shaving (4000) and cloud schedule adjustments (4001). Every distinct setpoint you dispatch produces its own entry, and each entry records the value before your override took over — which is what makes a dispatch independently verifiable after the fact.
Errors and troubleshooting
| Status | Cause | What to do |
|---|---|---|
200 | Delivered; body is the device reply. | — |
400 | Payload failed validation — a missing field, or value not a number. | Fix the body; the message names the field. |
401 | Missing, malformed or revoked API key. | Check the X-Api-Key header. |
403 | Key lacks edge-device:read_write, or is not authorized for this organization or the device's site. | Re-scope the key; confirm {orgId} matches the key's organization. |
499 | The device rejected the command — e.g. Datapoint not found, to is in the past, longer than 24h. | Read the message; this is a device-reported error, not a network fault. |
502 | The device or its gateway is offline. | Retry with backoff once it reconnects. |
- Send a unique
reqIdon every request and match it to the response, so a retry can be told apart from a duplicate dispatch in your logs. - A
499ondemandResponseOverridemeans nothing was applied — the whole command is rejected atomically, so it is safe to correct and resend. - If a dispatch returns
200butgetTechnicalConnectionValuesdoes not showlockedBy: "demand-response", check the window: afromin the future is accepted and queued, not applied.