Bringing your own data via pipeline endpoints
So far you have read data. In this tutorial you send data into OctoMesh, for example the readings of your own meter, a battery or a simulated device. Solutions on OctoMesh expose such inputs as HTTP endpoints that are implemented as pipelines on the Mesh Adapter. You learn how such an endpoint works, how it is secured and how to call it.
The energy-community endpoints below require EnergyCommunity.Base 2.11 or
newer and a Mesh Adapter release that contains the meter-reporting nodes
(RegisterSelfReportedMeteringPoint@1, PrepareMeterReadings@1,
CommunityEnergyBalance@1). On older tenants the routes do not exist.
Unlike tutorials 1 to 4, this tutorial writes data. Work in a test or lab tenant, not in a productive one.
The pattern: an HTTP request triggers a pipeline
- A pipeline starts with the trigger node
FromHttpRequest@2, which registers a path and an HTTP method on the Mesh Adapter of the tenant. - A request to
https://<mesh-adapter-host>/<tenant>/<path>runs the pipeline. The JSON request body is available to the nodes at$.body. - The nodes validate the input, resolve the referenced entities and write, for example with a node that saves values into a stream-data archive.
- Whatever the pipeline leaves in its data context is returned as the JSON response. Well-designed endpoints reduce it to a small result object.
A trigger looks like this:
triggers:
- type: FromHttpRequest@2
path: /meterReadings
method: POST
requiredRoles:
- MeterReporter
allowAnonymous: false
Authentication
FromHttpRequest@2 requires a valid access token of the tenant and, if
requiredRoles is set, one of the listed roles. Requests without a token, with a
token of another tenant or without the role are rejected with 401 or 403
before the pipeline runs.
A device or service has no user who could log in in a browser. It authenticates as an OAuth client with the client credentials flow:
- An administrator of the tenant creates a client-credentials client and assigns it the role the endpoint requires (see Clients and API Scopes). You receive a client id and a secret. Store the secret like a password.
- Your program requests a token. The parameter
acr_values=tenant:<tenant>is mandatory: clients are stored per tenant, and without it the identity service looks in the wrong place and answersinvalid_client.
curl -s https://connect.<your-domain>/connect/token \
-d grant_type=client_credentials \
-d client_id=<client-id> \
-d client_secret="$CLIENT_SECRET" \
-d scope=octo_api \
-d acr_values=tenant:<tenant>
import os, requests
def client_token() -> str:
response = requests.post(
"https://connect.<your-domain>/connect/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"],
"scope": "octo_api",
"acr_values": f"tenant:{os.environ['OCTO_TENANT']}",
},
timeout=30,
)
response.raise_for_status()
return response.json()["access_token"]
The token is valid for a limited time (expires_in in the response). Request a
new one before it expires instead of requesting one per call.
Calling an endpoint
import os, requests
ADAPTER = "https://<mesh-adapter-host>"
TENANT = os.environ["OCTO_TENANT"]
def call(path: str, payload: dict) -> dict:
response = requests.post(
f"{ADAPTER}/{TENANT}{path}",
json=payload,
headers={"Authorization": f"Bearer {client_token()}"},
timeout=30,
)
response.raise_for_status() # 401/403: token, tenant or role problem
result = response.json()
if result.get("status") not in ("ok", "partial"):
raise RuntimeError(result.get("message"))
return result
Response convention
The energy-community endpoints answer with HTTP 200 whenever the request reached the pipeline, and report the outcome in the body:
{ "status": "ok", "message": "..." }
status is ok, partial (some items accepted, some rejected, with details) or
error. As with GraphQL (see tutorial 2),
an HTTP 200 alone does not mean success: always read status.
Energy-community endpoints
The EnergyCommunity solution provides three endpoints for metering points whose data does not come from the grid operator (for example your own meter, a battery modelled as two metering points for charging and discharging, or a simulated device):
| Endpoint | Purpose |
|---|---|
POST /registerMeteringPoint | Register a metering point (consumption or production) with its customer, facility and participation in the community. Idempotent. |
POST /meterReadings | Report 15-minute energy values (kWh) for one or more registered metering points. Values can be corrected until the daily reporting cut-off. |
POST /communityBalance | Read the provisional balance of the community for the last completed slots: generation, consumption, allocated energy, surplus, coverage. Does not write. |
Values you report go into the raw archive as register 1-1:1.9.0 G.01
(consumption) or 1-1:2.9.0 G.01 (production). The allocation registers G.03,
G.01T and P.01T are computed by the community's daily allocation, never
reported (see tutorial 3).
Register a metering point
Role MeterReporter. The call creates (or reuses) the customer, one
operating facility, the metering point with data source SelfReported, the raw
anchor of its direction (1-1:1.9.0 G.01 for consumption, 1-1:2.9.0 G.01 for
production) and one participation period. It is idempotent: calling it again
with the same number and direction answers created: false with the same rtIds
and updates displayName, participationFactor and sizes; the participation
start never moves.
- Only numbers in the reserved self-report range (
AT009998…) are accepted, so a client can never take over a metering point that comes from the grid operator or the simulation. zipcode(optional) places the facility in a city from the Locations.Austria blueprint; without that blueprint the call answersCITY_NOT_FOUND.
curl -s -X POST "https://adapter.<your-domain>/<tenant>/registerMeteringPoint" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"meteringPointNumber": "AT0099980000000010100000000000001",
"direction": "consumption",
"displayName": "Battery group 1 (charging)",
"participationFactor": 100,
"zipcode": 5020
}'
{
"status": "ok",
"message": "Metering point registered.",
"created": true,
"meteringPoint": {
"rtId": "6b0c…",
"anchors": [{ "obisCode": "1-1:1.9.0 G.01", "rtId": "6b0c…" }],
"participation": { "from": "2026-11-13", "factor": 100 }
}
}
Report meter readings
Role MeterReporter. Values are addressed by metering point number and
direction, never by rtId; you cannot name an OBIS code. Each value covers exactly
one 15-minute slot on the UTC grid and is stored with data quality L1 on the raw
register of its direction. Until the daily cut-off (the community's
allocation cut-off, by default 06:00 Europe/Vienna on the next day) a later
report replaces an earlier one; after that the day is frozen.
Every value is checked on its own; valid values are written even when others are
rejected (status: partial). Rejection codes: UNKNOWN_METERING_POINT,
NOT_SELF_REPORTED, OFF_GRID (not on the 15-minute grid), OUT_OF_RANGE,
IN_FUTURE, NOT_PARTICIPATING, DAY_FROZEN, DUPLICATE_SLOT. frozenUntil
tells you up to which point in time the data is already final.
curl -s -X POST "https://adapter.<your-domain>/<tenant>/meterReadings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"readings": [{
"meteringPointNumber": "AT0099980000000010100000000000001",
"direction": "consumption",
"values": [
{ "from": "2026-11-13T08:00:00Z", "to": "2026-11-13T08:15:00Z", "kWh": 1.25 },
{ "from": "2026-11-13T08:15:00Z", "to": "2026-11-13T08:30:00Z", "kWh": 1.10 },
{ "from": "2026-11-13T08:30:00Z", "to": "2026-11-13T08:45:00Z", "kWh": 0.95 },
{ "from": "2026-11-13T08:40:00Z", "to": "2026-11-13T08:55:00Z", "kWh": 0.80 }
]
}]
}'
{
"status": "partial",
"message": "3 of 4 values accepted.",
"accepted": 3,
"rejected": [
{ "reading": 0, "value": 3, "code": "OFF_GRID", "message": "…" }
],
"frozenUntil": "2026-11-12T23:00:00Z"
}
Read the community balance
Roles MeterReporter or EnergyCommunityReporting. Read-only: returns
the provisional balance of the last closed slots (slots, default 4, at most 96).
It uses the same allocation logic as the daily run, so a slot with complete inputs
matches the final result. With meteringPoints the answer also contains the
registers of those metering points.
| Field | Meaning |
|---|---|
productionKwh / consumptionKwh | Total generation and consumption of the community in the slot |
allocatedKwh | Energy shared inside the community (sum of G.03) |
surplusKwh | Generation not used inside the community (sum of P.01T) |
shortfallKwh | Consumption not covered by the community |
coverage | Share of consumption covered by the community (0–1) |
reported | Metering points expected, received, and how many of them are simulated |
simulatedIncluded | false if a simulated member has no value in the returned slots yet |
Missing values count as 0; reported.received lower than reported.expected
shows that the slot is still incomplete.
curl -s -X POST "https://adapter.<your-domain>/<tenant>/communityBalance" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "slots": 4, "meteringPoints": ["AT0099980000000010100000000000001"] }'
{
"status": "ok",
"message": "…",
"generatedAt": "2026-11-13T09:01:12Z",
"provisional": true,
"simulatedIncluded": true,
"slots": [
{
"from": "2026-11-13T08:00:00Z",
"productionKwh": 42.1,
"allocatedKwh": 31.5,
"surplusKwh": 10.6,
"consumptionKwh": 42.0,
"shortfallKwh": 10.5,
"coverage": 0.749,
"reported": { "expected": 19, "received": 19, "simulated": 16 }
}
]
}
Check the result
After reporting, verify that your values arrived with a stream-data query as in tutorial 2: look up the anchor of your metering point and register, then read the slots you reported.
End to end, a client such as a battery controller does this:
- Register its metering points once (
/registerMeteringPoint, one per direction: charging = consumption, discharging = production). - Report every 15 minutes or in batches, at the latest one full day of 96
values before the cut-off (
/meterReadings), and checkrejected. - Watch the provisional balance (
/communityBalance) to decide when to charge or discharge. - Read the final result on the next day after the allocation run: the
registers
1-1:2.9.0 G.03(consumer) or1-1:2.9.0 G.01T/P.01T(producer) of your metering point via GraphQL, as in tutorial 3.
Good practice for clients
- Send whole slots on the grid. 15-minute windows start at
:00,:15,:30and:45, with explicit time zone (Zor an offset). - Batch. Send many values per call instead of one call per value.
- Retry safely. Reporting the same slot again replaces the value until the cut-off, so retries after a timeout are harmless.
- Keep secrets out of code. Read the client secret from the environment or a secret store, never commit it, never log a token.
- Watch the response. Log
statusandmessage; alert onerrorand on rejected items inpartialresponses.
Building your own endpoint
Any tenant with a Mesh Adapter can host its own endpoints. Write a pipeline with
FromHttpRequest@2, protect it with requiredRoles, validate the input with
If@1 nodes, write with the save nodes of your target (runtime entities or
archives) and shape the response with Project@1. See
Data flows and nodes and
Pipeline identity.
Summary
- Pipeline endpoints are HTTP routes on the Mesh Adapter, defined by a
FromHttpRequest@2trigger. - Devices and services authenticate with client credentials; the token request
needs
acr_values=tenant:<tenant>, the client needs the endpoint's role. - Responses are HTTP 200 with a
statusfield; read it.