MyMeter — API reference v1

BEN MyMeter API

Authentication & limits

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

StatusDescriptionBody
200 API reachable and token valid
FieldTypeDescription
status string
application string
time string
401 Missing, unknown, revoked or expired token
FieldTypeDescription
message string
429 Per-token rate limit exceeded
FieldTypeDescription
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

FieldTypeDescription
readings * array
  • One of:
    FieldTypeDescription
    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:
    FieldTypeDescription
    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.

Example: hourly-consumption

{
    "readings": [
        {
            "meter": "MTR-00012345",
            "kind": "consumption",
            "value": 1.25,
            "unit": "kwh",
            "recorded_at": "2026-08-15T10:00:00Z"
        }
    ]
}

Example: saldo

{
    "readings": [
        {
            "meter": "MTR-00012345",
            "kind": "saldo",
            "value": 4521.75,
            "unit": "kwh",
            "recorded_at": "2026-08-15T10:00:00Z"
        }
    ]
}

Example: izar-register-with-status

{
    "readings": [
        {
            "meter": "IZAR-88231",
            "profile": "izar",
            "unit": "m3",
            "payload": {
                "value": 4521.5,
                "status": [
                    "leakage",
                    "battery_low"
                ]
            },
            "recorded_at": "2026-08-15T10:00:00Z"
        }
    ]
}

Example: pulse-counter

{
    "readings": [
        {
            "meter": "PULSE-7",
            "profile": "pulse_counter",
            "payload": {
                "pulses": 12845
            },
            "recorded_at": "2026-08-15T10:00:00Z"
        }
    ]
}

Responses

StatusDescriptionBody
201 At least one reading stored
FieldTypeDescription
accepted integer newly stored readings
duplicates integer retried reports, ignored
rejected integer items refused per item: provisioning cap or unconfigured meter
results array
FieldTypeDescription
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)
FieldTypeDescription
accepted integer newly stored readings
duplicates integer retried reports, ignored
rejected integer items refused per item: provisioning cap or unconfigured meter
results array
FieldTypeDescription
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
FieldTypeDescription
message string
403 Token lacks the `data:report` scope
FieldTypeDescription
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.
FieldTypeDescription
message string
errors object
429 Per-token rate limit exceeded
FieldTypeDescription
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

StatusDescriptionBody
200 Meter list
FieldTypeDescription
data array
FieldTypeDescription
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.
FieldTypeDescription
key string
version integer
name string
needs_review boolean
group object, null null = the "Ungrouped meters" bucket
FieldTypeDescription
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`.
FieldTypeDescription
statuses array string leak | 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
FieldTypeDescription
message string
403 Token lacks the scope required by this endpoint
FieldTypeDescription
message string
429 Per-token rate limit exceeded
FieldTypeDescription
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

NameTypeDescription
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

StatusDescriptionBody
200 Readings page
FieldTypeDescription
aggregate string
raw | hourly | daily
from string
to string
data array
  • One of:
    FieldTypeDescription
    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
    canonical_unit string, null
    kwh | m3 | null
    quality_flags array, null Data quality flags (#29, see ADR 0007); null = clean string unit_mismatch | negative_consumption | implausible_rate | saldo_mismatch | saldo_decrease | rollover_wrap | meter_exchange
    recorded_at string
    recorded_at_local string Only present when tz was given — recorded_at in that zone
  • One of:
    FieldTypeDescription
    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:
    FieldTypeDescription
    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
FieldTypeDescription
current_page integer
per_page integer
total integer
last_page integer
401 Missing, unknown, revoked or expired token
FieldTypeDescription
message string
403 Token lacks the scope required by this endpoint
FieldTypeDescription
message string
422 Validation failed (unknown meter/group, bad range, …)
FieldTypeDescription
message string
429 Per-token rate limit exceeded
FieldTypeDescription
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

NameTypeDescription
meter_id integer Filter to one meter
from string
to string
page integer
default: 1
per_page integer
default: 100

Responses

StatusDescriptionBody
200 Status report page
FieldTypeDescription
data array
FieldTypeDescription
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 string leak | 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
FieldTypeDescription
current_page integer
per_page integer
total integer
last_page integer
401 Missing, unknown, revoked or expired token
FieldTypeDescription
message string
403 Token lacks the scope required by this endpoint
FieldTypeDescription
message string
422 Validation failed (unknown meter, …)
FieldTypeDescription
message string
429 Per-token rate limit exceeded
FieldTypeDescription
message string

Error reference

Errors are JSON objects with a "message" field. The dedicated statuses:

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