Submit signed meter readings for a registered device.
Bearer + Device signatureYields up to L21,000 req/minIdempotent
Request body
FieldTypeReq.Description
device_idstring✓Registered device
readings[]array✓Max 1,000 per batch, ascending by device_ts
readings[].device_tsRFC 3339✓Device clock. Drift beyond ±300 s is flagged, not rejected
readings[].energy_wh_cumulativeinteger✓Lifetime counter. Monotonically non-decreasing within a meter_epoch
readings[].meter_epochinteger✓Increments on meter replacement or counter reset. The counter is monotonic inside one epoch and says nothing across two
readings[].sequence_numberinteger✓Monotonic within the device. Does not reset across meter_epoch — it is what orders readings when the energy counter cannot
readings[].reading_idstring✓Device-assigned identifier. What a correction points at
readings[].reset_reasonenum—meter_replaced · counter_rollover · device_reset · manual_correction. Set on the first reading of a new meter_epoch, null otherwise
readings[].measurement_qualityenum—good · estimated · substituted · suspect. Defaults to good
readings[].supersedes_reading_idstring—Present only on corrections. See POST /v1/telemetry:correct
readings[].active_power_winteger—Instantaneous active power
readings[].status_flagsstring[]—Device fault codes, passed through unmodified
ℹ Monotonicity rule
The monotonic constraint holds within a meter_epoch. It does not hold across epochs, and requiring it to would make every legitimate meter change look like an attack. Meter replacement, counter rollover, numeric overflow, device reset, and reading correction all push a cumulative counter down legitimately. On a swap, submit one reading carrying reset_reason: meter_replaced with the outgoing meter's final value and the incoming meter's initial value — register it first through POST /v1/devices/{id}:meter-swap — and cumulative energy stays continuous across the boundary.
Response · 200 OK
FieldTypeDescription
accepted / rejectedintegerCounts. Check rejected on every response
batch_idstringtbx_ reference
errors[]arrayPer-reading failures with index and ark_code
verification.signatureenumvalid · invalid · absent. Synchronous — decidable on receipt
verification.continuityenumok · gap · regression. A comparison against the previous reading in the same meter_epoch
verification.anomaly_statusenumprovisional · confirmed. The baseline model scores immediately; that score is provisional until review or the epoch closes
attestation.statusenumpending · anchored · failed. Anchoring is batched per epoch and is never synchronous
attestation.expected_atRFC 3339When anchoring is expected
⚠ Cumulative, not interval
energy_wh_cumulative is a lifetime counter. Sending an interval value produces a continuity regression and a provisional downgrade. If your source emits intervals, accumulate on your side before submitting.
⚠ 200 does not mean all accepted
Batches accept partially. Read rejected and errors[] every time. A silently dropped reading becomes a continuity gap thirty days later, when it is much harder to diagnose.
⚠ anomaly_status: provisional is not a verdict
The score you get back is the baseline model's immediate opinion. Anomalies can be resolved, and resolved anomalies can still be excluded from verified generation. Do not wire a provisional score straight into an alerting rule that pages someone.
⚠ attestation is asynchronous, and this response never carries the anchor
Anchoring is batched per epoch. Wait for the attestation.anchored webhook rather than polling this endpoint or, worse, treating expected_at as a guarantee.
✓ Batch on a natural cadence
One request per five-minute interval is the sweet spot. Sub-minute batching burns rate limit without improving assurance; hourly delays attestation.
Errors
CodeHTTPMeaning
device_not_found404Unknown or deregistered device_id
signature_invalid401Signature failed verification against the registered public key
counter_regression422Cumulative value below the last accepted reading in the same meter_epoch
meter_epoch_invalid422Epoch not registered, or lower than the device's current epoch
sequence_number_regression422Sequence number not monotonic within the device
batch_too_large413More than 1,000 readings
baseline_incomplete409Device still in its baseline window; readings stored but not attestable
{
"error": "counter_regression",
"message": "value below last accepted in meter_epoch 2",
"last_accepted_wh": 4182339000,
"hint": "register a meter swap first if the meter changed"
}