Developers / Ingest referenceSearch docs ⌘K
Reference

Ingest reference

Send BMS and GPS telemetry from any vendor to one endpoint. This page is the contract: how you authenticate, the vendor-neutral generic_json payload, batching, limits, and every response code. For a device already speaking a binary dialect, see the protocol catalog.

Overview

Ingest is the one-way door for raw readings. Every reading enters through a single endpoint, POST /v1/ingest/{protocol}, where the {protocol} path segment selects the codec that decodes your wire format. The simplest path is generic_json, a vendor-neutral JSON shape any backend can produce, and it is what this page documents in full. Devices that already speak a binary dialect (JT/T 808, GT06, Modbus, OCPP, …) post their native bytes to the same endpoint; the protocol catalog lists all eleven.

Tenant comes from the token. You never put a tenant in the body. The bearer token you authenticate with is bound to one tenant, and a reading can only ever land in that tenant's twin store. There is no cross-tenant ingest path.

Writes are idempotent on (tenant, device, timestamp): a replayed frame is a no-op, so a device can retry freely after a connectivity gap without double-counting.

Authentication

Ingest requires a bearer token that carries the ingest role. You mint one from a normal operator session: two calls, then you're sending data. You can also create one in the dashboard under Settings → API tokens.

1. Log in to obtain a session token (skip this if you already have a dashboard session):

log in
01# 1: Log in once to get a session token (or copy your dashboard session).
02curl -X POST https://api.rootd.cc/v1/auth/login \
03 -H "Content-Type: application/json" \
04 -d '{"email":"you@operator.com","password":"••••••••"}'
05# → { "token": "<SESSION_TOKEN>", "user": { "tenant": "...", "roles": [...] } }

2. Mint an ingest token. Machine-to-machine tokens always carry the ingest role on top of your account's roles, scoped to your tenant. Hand this token to the device or gateway that pushes telemetry, not your login password.

mint an ingest token
01# 2: Mint a long-lived ingest token. It carries the `ingest` role and is
02# bound to your tenant. Treat it as a device secret. (Dashboard: Settings →
03# API tokens does the same thing.)
04curl -X POST https://api.rootd.cc/v1/settings/tokens \
05 -H "Authorization: Bearer $SESSION_TOKEN" \
06 -H "Content-Type: application/json" \
07 -d '{"scopes":[],"ttlHours":720}'
08# → { "token": "<INGEST_TOKEN>" }
Treat the ingest token as a secret. Anyone holding it can write telemetry into your tenant. Rotate it by minting a new one; set a bounded ttlHours for short-lived deployments.

The endpoint

POST/v1/ingest/{protocol}ingest gateway

Base URL: https://api.rootd.cc. Send the native payload as the raw request body: a JSON document for generic_json and ocpp, or raw protocol bytes for the binary codecs.

FieldTypeDescription
{protocol} reqpathCodec name. generic_json for JSON; see the catalog for binary dialects.
Authorization reqheaderBearer token with the ingest role (see above).
Content-Type headerapplication/json for generic_json; binary protocols post raw bytes.
device queryDevice id for protocols whose wire carries none (e.g. modbus, can). For generic_json it is only a fallback when a message omits device.
body reqrawThe native payload. For generic_json, one JSON object or an array of them. Max 1 MiB.

generic_json schema

A generic_json payload is one object, or a JSON array of objects. Each object describes one device at one instant. Optional numeric fields are omitted when absent: a GPS-only ping carries no voltage; a battery-only ping carries no coordinates.

FieldTypeDescription
device reqstringDevice id. Also the battery id unless battery is set. Falls back to ?device= if omitted.
battery stringExplicit battery id: set it when one device hosts more than one pack.
timestamp string (RFC 3339)Sample time in UTC. Omitted → the server's arrival time is used.
v numberPack voltage, volts.
i numberPack current, amps. Negative = discharge.
temp numberPack temperature, °C.
soc numberVendor-reported state of charge, 0..1 (raw). Rootd derives its own estimate on read; this is a hint.
events string[]Markers: swap, fault, or connectivity_transition.
lat / lon numberGPS fix in decimal degrees. Both required to record a location.
speed numberGround speed, km/h.
heading numberHeading in degrees; 0 = north.
altitude numberAltitude in metres.
What gets emitted. A battery reading is recorded when any of v / i / temp / soc is present. A location is recorded only when both lat and lon are present. A message may carry both, either, or just a heartbeat (accepted with zero counts).
single reading
01# 3: Push a reading. Tenant comes from the token, never the body.
02curl -X POST https://api.rootd.cc/v1/ingest/generic_json \
03 -H "Authorization: Bearer $INGEST_TOKEN" \
04 -H "Content-Type: application/json" \
05 -d '{"device":"BIKE-001","timestamp":"2026-06-24T09:00:00Z",
06 "v":63.2,"i":-4.5,"temp":29,"soc":0.71}'
07# → 202 { "acceptedReadings": 1, "acceptedLocations": 0 }

Batches & limits

To send many readings in one request, post a JSON array instead of a single object:

batch ingest (array)
01# Send many at once: post a JSON array. Each element is one message.
02curl -X POST https://api.rootd.cc/v1/ingest/generic_json \
03 -H "Authorization: Bearer $INGEST_TOKEN" \
04 -H "Content-Type: application/json" \
05 -d '[{"device":"BIKE-001","timestamp":"2026-06-24T09:00:00Z","v":63.2,"i":-4.5,"temp":29,"soc":0.71},
06 {"device":"BIKE-002","timestamp":"2026-06-24T09:00:05Z","v":61.8,"i":2.0,"temp":33,"soc":0.40,
07 "lat":-1.286,"lon":36.817,"speed":18}]'
08# → 202 { "acceptedReadings": 2, "acceptedLocations": 1 }
FieldTypeDescription
Body size limit1 MiB per request. Larger → 413.
Readings limitUp to 1000 battery readings decoded per request.
Locations limitUp to 1000 GPS fixes decoded per request.
Retention policyRaw readings are retained 90 days; older data is served from hourly rollups via the Twin API.

Responses & errors

A successful POST returns 202 Accepted with the counts decoded from the payload. The whole request is atomic per message: the first invalid reading rejects the request and dead-letters the offending payload for inspection, so nothing is silently dropped.

202 Accepted
01{ "acceptedReadings": 2, "acceptedLocations": 1 }

Errors return a JSON body naming the reason, the protocol, and the offending field:

error body
01{
02 "error": "reading_out_of_bounds",
03 "protocol": "generic_json",
04 "field": "temperature"
05}
FieldTypeDescription
400 bad_requestEmpty body (empty_payload), unreadable, or malformed JSON.
401 unauthorizedMissing or invalid bearer token.
403 forbiddenToken lacks the ingest role.
404 not_foundUnknown {protocol} (unknown_protocol) or unknown tenant (unknown_tenant).
413 payload_too_largeBody over 1 MiB, or over 1000 readings/locations.
422 unprocessableDecode failed (decode_failed) or a reading failed validation (invalid_reading / reading_out_of_bounds, with field). Payload is dead-lettered.
429 rate_limitedPer-tenant rate limit. Honor Retry-After.
503 broker_unavailableIngest queue temporarily unavailable. Honor Retry-After and retry; the write is idempotent.

Connectivity & freshness

Rootd models connectivity from the gap between now and a battery's latest reading. A dropped feed is a state, never an error or a deleted twin:

FieldTypeDescription
online stateA recent reading within the expected interval.
stale stateNo reading for a configurable window; the 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.

Where to next

  • Protocols: the eleven codecs (telematics, BMS/bus, charging) and a curl example for each binary dialect.
  • Twin API: read the twin back to confirm your readings landed, then consume health, forecasts, and rider projections.
  • Security: how tenant isolation and audited reads work.
  • Talk to us: to provision a tenant or add a protocol.