Online Metering Data Collection Platform — machine/IoT API.
Authentication: every request carries `Authorization: Bearer <token>`.
Tokens are created in *Settings → API tokens*, are bound to exactly one
tenant, and carry scopes. Reporting requires the `data:report` scope;
the read/query endpoints require `data:read`.
Rate limits: 60 requests/minute per token. Responses include
`X-RateLimit-Limit` / `X-RateLimit-Remaining`.
bearerAuth: Http bearer — API token from Settings → API tokens (prefix `bmm_`)
GET/api/v1/ping
Authenticated liveness probe
Responses
Status
Description
Body
200
API reachable and token valid
Field
Type
Description
status
string
application
string
time
string
401
Missing, unknown, revoked or expired token
Field
Type
Description
message
string
429
Per-token rate limit exceeded
Field
Type
Description
message
string
POST/api/v1/reportData
Report consumption / saldo readings (batch)
Reports one or many readings for meters of the token's tenant.
- Meter identifiers unknown to the tenant are **auto-provisioned**
(filed under "Ungrouped meters"). Auto-provisioning is bounded
per tenant (default 100 new meters/hour); beyond the cap, items
for NEW identifiers return `status: rejected_provision_limit`
while readings for existing meters are unaffected.
- On the hosted service a **plan meter limit** applies as well.
At the limit, items for NEW identifiers return
`status: rejected_plan_limit` and the workspace owner needs to
upgrade. Readings for meters that ALREADY EXIST are never
refused for a plan or payment reason — meter history cannot be
reconstructed once a device has moved on, so the commercial
lever is access to the data, never acceptance of it.
- **Idempotent:** a reading is identified by
`(meter, kind, recorded_at)`; retries of the same report are
answered with `status: duplicate` and never double-count.
- **Model-aware:** every item is validated against the meter's
assigned device profile (see `profile` on `/meters`). Meters
on the *generic* profile report the plain
`kind`/`value`/`unit` shape; meters assigned another model
report a `payload` object matching that profile's schema
(e.g. the IZAR register reading plus status flags). The
payload is normalized into canonical readings; the raw
payload is retained on the stored reading for traceability.
- **Pulse counters** (#44) report `pulses` (absolute counter)
and/or `pulse_delta` (increment since the last report); the
per-meter **pulse weight** ("1 pulse = weight × unit",
configured in the meter settings) converts counts into
measurements. Items for meters without that configuration
are answered per item with `status: rejected_unconfigured` —
the meter itself IS provisioned, so it can be configured and
the next report ingests. Multi-channel pulse devices report
each channel as its own meter identifier.
- The optional `profile` field declares the device's model
explicitly: an unknown meter is then auto-provisioned
directly onto that profile (nothing left to review), and a
meter still on the unconfirmed provisioning default is
upgraded to the declared model. A declaration that
contradicts a *user-confirmed* profile assignment is
rejected — reassign the meter in settings first.
- **Atomic validation:** the whole batch is validated before
anything is stored. Any schema violation rejects the entire
request with a 422 naming the offending field
(e.g. `readings.3.payload.value`); nothing is written.
Request body
Field
Type
Description
readings*
array
One of:
Field
Type
Description
meter*
string
Device identifier, unique within the tenant
profile
string
Optional explicit model declaration (catalog key)
kind*
string
consumption | saldo
`consumption` = incremental delta since the previous report; `saldo` = absolute register/balance value.
value*
number
unit*
string
kwh | wh | m3 | l
recorded_at*
string
Device-side measurement time (ISO 8601). Must not lie more than 5 minutes in the future.
One of:
Field
Type
Description
meter*
string
Device identifier, unique within the tenant
profile
string
Optional explicit model declaration (catalog key). Unknown meters are provisioned onto this profile; unconfirmed provisioning defaults are upgraded to it; a mismatch with a user-confirmed assignment is rejected with 422.
payload*
object
Profile-schema fields, e.g. IZAR: value (register reading, number) + status (list of flag strings)
unit
string
kwh | wh | m3 | l
Unit of the payload's measurement fields
recorded_at*
string
Device-side measurement time (ISO 8601). Must not lie more than 5 minutes in the future.
items refused per item: provisioning cap or unconfigured meter
results
array
Field
Type
Description
meter
string
kind
string
consumption | saldo
recorded_at
string
status
string
created | duplicate | rejected_provision_limit | rejected_plan_limit | rejected_unconfigured
rejected_unconfigured (#44): the meter cannot convert this item yet (pulse report without a configured pulse weight); configure the meter and resend. rejected_provision_limit (#13): too many NEW meters this hour; existing meters are unaffected, retry later. rejected_plan_limit (#32): the workspace is at its plan's meter limit; an owner has to upgrade. Both reject only the creation of a new meter — readings for meters that already exist always ingest.
200
Nothing new — every entry was a duplicate (retry)
Field
Type
Description
accepted
integer
newly stored readings
duplicates
integer
retried reports, ignored
rejected
integer
items refused per item: provisioning cap or unconfigured meter
results
array
Field
Type
Description
meter
string
kind
string
consumption | saldo
recorded_at
string
status
string
created | duplicate | rejected_provision_limit | rejected_plan_limit | rejected_unconfigured
rejected_unconfigured (#44): the meter cannot convert this item yet (pulse report without a configured pulse weight); configure the meter and resend. rejected_provision_limit (#13): too many NEW meters this hour; existing meters are unaffected, retry later. rejected_plan_limit (#32): the workspace is at its plan's meter limit; an owner has to upgrade. Both reject only the creation of a new meter — readings for meters that already exist always ingest.
401
Missing, unknown, revoked or expired token
Field
Type
Description
message
string
403
Token lacks the `data:report` scope
Field
Type
Description
message
string
422
Validation failed — structural or against the meter's profile schema. `errors` keys name the offending field (e.g. `readings.3.payload.value`); the whole batch was rejected, nothing was stored.
Field
Type
Description
message
string
errors
object
429
Per-token rate limit exceeded
Field
Type
Description
message
string
GET/api/v1/meters
List the tenant's meters
Meter inventory of the token's tenant, ordered by identifier.
Requires the `data:read` scope. Use the returned `id` values to
filter `/readings`.
Responses
Status
Description
Body
200
Meter list
Field
Type
Description
data
array
Field
Type
Description
id
integer
meter_identifier
string
name
string, null
unit
string, null
profile
object, null
Device profile (#41) — which meter model's data format this meter reports. `needs_review: true` means the profile was defaulted at auto-provisioning and has not been confirmed by a user yet.
Field
Type
Description
key
string
version
integer
name
string
needs_review
boolean
group
object, null
null = the "Ungrouped meters" bucket
Field
Type
Description
id
integer
name
string
status
object, null
Latest device-reported status (#45), normalized to the canonical vocabulary. Empty `statuses` = the device reports all-clear; null = this meter never reported status. History (including raw provider flags): `/statusReports`.
Field
Type
Description
statuses
array
stringleak | burst | dry | reverse | tamper | low_battery | error— Canonical device-status vocabulary (#45): every device profile maps its raw flags onto this set. Raw flags a profile cannot interpret map to `error` (surfaced, never dropped).
reported_at
string
401
Missing, unknown, revoked or expired token
Field
Type
Description
message
string
403
Token lacks the scope required by this endpoint
Field
Type
Description
message
string
429
Per-token rate limit exceeded
Field
Type
Description
message
string
GET/api/v1/readings
Query readings (raw or aggregated)
Readings of the token's tenant within a time range, optionally
filtered to one meter or one group subtree and/or one kind.
Requires the `data:read` scope.
- Defaults: `to` = now, `from` = `to` − 7 days; the range is
capped at 366 days.
- `aggregate=raw` returns individual readings (oldest first),
including canonical values and quality flags (#29).
- `aggregate=hourly|daily` returns one bucket per
(bucket, meter, kind) with `sum`, `min`, `max`, `avg` and
`reading_count`, computed over CANONICAL values (kwh / m3,
#29) — readings without a canonical value (unit mismatch,
not yet backfilled) are excluded from buckets. For
`consumption` the meaningful figure is usually `sum`; for
`saldo` (a counter state) use `min`/`max`/`avg` — summing
counter states is meaningless.
- `tz=<IANA zone>` buckets in local wall time — daily buckets
on DST-change days correctly span 23/25 hours; `bucket_start`
is then local time. Raw rows gain `recorded_at_local`.
Storage and defaults stay UTC.
- `derive=consumption` (requires `meter_id`, no `aggregate`)
returns the consumption series derived from consecutive
SALDO readings, bridging counter rollovers and recorded
meter exchanges (#29). Intervals longer than the meter's
expected cadence are marked `gap: true`;
`interpolate=linear` (opt-in) splits such deltas into
cadence slots, each marked `interpolated: true`. Unknown
intervals (exchange without baselines, unexplained decrease)
have `consumption: null` — values are never invented.
Query parameters
Name
Type
Description
meter_id
integer
Filter to one meter (mutually exclusive with group_id)
group_id
integer
Filter to a group INCLUDING its subtree
kind
string
consumption | saldo
from
string
to
string
aggregate
string
raw | hourly | daily
default: 'raw'
tz
string
IANA timezone for bucketing/presentation (#29), e.g. Europe/Vienna
derive
string
consumption
Derive consumption from saldo readings of one meter (#29)
interpolate
string
linear
Only with derive: split gap deltas into cadence slots, marked interpolated
page
integer
default: 1
per_page
integer
default: 100
Responses
Status
Description
Body
200
Readings page
Field
Type
Description
aggregate
string
raw | hourly | daily
from
string
to
string
data
array
One of:
Field
Type
Description
meter_id
integer
meter_identifier
string
kind
string
consumption | saldo
value
number
Raw value exactly as reported
unit
string, null
canonical_value
number, null
Value in the family's canonical unit (#29); null = unit mismatch or not backfilled
Only present when tz was given — recorded_at in that zone
One of:
Field
Type
Description
bucket_start
string
Bucket start, "YYYY-MM-DD HH:MM:SS" in UTC
meter_id
integer
kind
string
consumption | saldo
reading_count
integer
sum
number
min
number
max
number
avg
number
One of:
Field
Type
Description
period_start
string
period_end
string
consumption
number, null
Canonical units; null = unknown interval
unit
string, null
kwh | m3 | null
gap
boolean
Interval exceeds the meter's expected cadence
rollover
boolean
Delta bridges a counter rollover
exchange
boolean
Delta bridges a recorded meter exchange
unknown
boolean
True when consumption could not be derived
interpolated
boolean
Row was synthesized by interpolate=linear
meta
object
Field
Type
Description
current_page
integer
per_page
integer
total
integer
last_page
integer
401
Missing, unknown, revoked or expired token
Field
Type
Description
message
string
403
Token lacks the scope required by this endpoint
Field
Type
Description
message
string
422
Validation failed (unknown meter/group, bad range, …)
Field
Type
Description
message
string
429
Per-token rate limit exceeded
Field
Type
Description
message
string
GET/api/v1/statusReports
Query device-status history
Device-status reports of the token's tenant, newest first.
Requires the `data:read` scope.
Each row is one status the device itself reported (#45): the
provider's raw flags verbatim plus the normalized canonical
codes every profile maps onto. An empty `statuses` list means
the device reported all-clear — a leak appearing and later
clearing is therefore two traceable rows. Device status is
distinct from the platform-computed data-quality flags on
`/readings` (#29); both surface as metering issues in the UI.
Query parameters
Name
Type
Description
meter_id
integer
Filter to one meter
from
string
to
string
page
integer
default: 1
per_page
integer
default: 100
Responses
Status
Description
Body
200
Status report page
Field
Type
Description
data
array
Field
Type
Description
meter_id
integer
meter_identifier
string
raw_flags
array
Provider flags verbatim, exactly as reported
string
statuses
array
Canonical codes; empty = device reported all-clear
stringleak | burst | dry | reverse | tamper | low_battery | error— Canonical device-status vocabulary (#45): every device profile maps its raw flags onto this set. Raw flags a profile cannot interpret map to `error` (surfaced, never dropped).
reported_at
string
meta
object
Field
Type
Description
current_page
integer
per_page
integer
total
integer
last_page
integer
401
Missing, unknown, revoked or expired token
Field
Type
Description
message
string
403
Token lacks the scope required by this endpoint
Field
Type
Description
message
string
422
Validation failed (unknown meter, …)
Field
Type
Description
message
string
429
Per-token rate limit exceeded
Field
Type
Description
message
string
Error reference
Errors are JSON objects with a "message" field. The dedicated statuses:
Case
Description
Unauthenticated
Missing, unknown, revoked or expired token
RateLimited
Per-token rate limit exceeded
MissingScope
Token lacks the scope required by this endpoint
ValidationFailed (422)
"message" plus an "errors" map of field name to messages — fix the request and retry.