Developers / Ingest referenceSearch docs ⌘K
Reference

Ingest reference

Send BMS telemetry from any vendor. Rootd's ingest is vendor-neutral — it accepts whatever each brand emits and normalizes units, timing, and gaps into one twin.

Overview

Ingest is the one-way door for raw readings. You don't standardize hardware first — you point each feed at Rootd and the normalization layer resolves the differences. Writes are idempotent on (tenant, pack_id, ts), so safe retries never double-count.

Tenant on every call. Every reading carries a tenant. Ingest is scoped at the boundary; a reading can only ever land in its own tenant's twin store.

Transports

The same logical payload arrives over whichever transport a vendor speaks. Rootd ingests:

  • REST — the canonical example used throughout these docs.
  • MQTT — publish readings to a tenant-scoped topic; same field schema.
  • CAN — via an edge collector that frames raw bus data into readings.

Payload schema

POST/v1/ingest/readingssingle reading
FieldTypeDescription
tenant reqstringYour tenant identifier. Scopes the reading.
pack_id reqstringStable identifier for the battery pack.
vendor reqstringSource BMS vendor key. Used to select the normalization profile.
ts reqstring (ISO-8601)Reading timestamp in UTC. Drives freshness.
metrics reqobjectRaw measured fields (e.g. voltage_mv, current_ma, temp_c). Units are resolved on normalization.

Send many at once with the batch endpoint:

batch ingest
01POST /v1/ingest/readings:batch
02{
03 "tenant": "northwind",
04 "readings": [
05 { "pack_id": "PK-0421", "vendor": "vendor_a",
06 "ts": "2026-06-03T08:14:02Z",
07 "metrics": { "voltage_mv": 53120, "current_ma": -8100, "temp_c": 31 } }
08 ]
09}

Normalization

Rootd maps each vendor's raw metrics onto a single internal schema: units are converted, sign conventions aligned, and missing-but-derivable values computed. The output is the vendor-neutral twin — any BMS in, one twin out.

State-of-health is derived from charge/discharge history. State-of-charge is then estimated on read from SoH and the latest reading — it is never written back as a stored field.

Connectivity & freshness

Rootd models connectivity from the gap between now and the latest ts:

FieldTypeDescription
online stateRecent reading within the expected interval.
stale stateNo reading for a configurable window; twin holds last-known values.
offline stateGap exceeds the offline threshold. Still queryable at last-known state.
Loss is a state, not a drop. A missing feed never deletes a twin. It transitions to stale or offline and keeps serving last-known values with an honest freshness flag.

Errors

FieldTypeDescription
400 bad_requestMalformed payload or missing required field.
401 unauthorizedMissing or invalid tenant credentials.
409 conflictNon-idempotent replay with a conflicting value for the same key.
422 unprocessableUnknown vendor profile — no normalization mapping available.