Skip to main content

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.

Requirements

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.

Use a test tenant

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​

  1. A pipeline starts with the trigger node FromHttpRequest@2, which registers a path and an HTTP method on the Mesh Adapter of the tenant.
  2. A request to https://<mesh-adapter-host>/<tenant>/<path> runs the pipeline. The JSON request body is available to the nodes at $.body.
  3. The nodes validate the input, resolve the referenced entities and write, for example with a node that saves values into a stream-data archive.
  4. 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:

  1. 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.
  2. 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 answers invalid_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):

EndpointPurpose
POST /registerMeteringPointRegister a metering point (consumption or production) with its customer, facility and participation in the community. Idempotent.
POST /meterReadingsReport 15-minute energy values (kWh) for one or more registered metering points. Values can be corrected until the daily reporting cut-off.
POST /communityBalanceRead 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 answers CITY_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.

FieldMeaning
productionKwh / consumptionKwhTotal generation and consumption of the community in the slot
allocatedKwhEnergy shared inside the community (sum of G.03)
surplusKwhGeneration not used inside the community (sum of P.01T)
shortfallKwhConsumption not covered by the community
coverageShare of consumption covered by the community (0–1)
reportedMetering points expected, received, and how many of them are simulated
simulatedIncludedfalse 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:

  1. Register its metering points once (/registerMeteringPoint, one per direction: charging = consumption, discharging = production).
  2. Report every 15 minutes or in batches, at the latest one full day of 96 values before the cut-off (/meterReadings), and check rejected.
  3. Watch the provisional balance (/communityBalance) to decide when to charge or discharge.
  4. Read the final result on the next day after the allocation run: the registers 1-1:2.9.0 G.03 (consumer) or 1-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, :30 and :45, with explicit time zone (Z or 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 status and message; alert on error and on rejected items in partial responses.

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@2 trigger.
  • 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 status field; read it.