Protocols
Send telemetry from any EV-motorbike OEM, manufacturer, or supplier in its native wire format. One endpoint, many codecs, across three layers: GPS telematics, on-vehicle BMS/bus, and charging infrastructure.
Overview
Rootd's ingest gateway decodes the wire format a device already speaks and normalizes it into the vendor-neutral twin. You don't reflash firmware or build a translation layer. You point the feed at POST /v1/ingest/{protocol} and pick the codec by name. Battery readings flow into the digital twin; GPS fixes into the fleet map.
generic_json for custom backends.Get a token
Authentication is a bearer token scoped to your tenant. An operator mints one from the dashboard (Settings → API tokens) or via the API; the token carries the ingest role, so it can push telemetry but is still tenant-isolated. Treat it as a device secret.
01# Mint an API token (an operator does this once; it carries the ingest role,02# scoped to your tenant). Or create one in the dashboard: Settings → API tokens.03curl -X POST https://api.rootd.cc/v1/settings/tokens \04 -H "Authorization: Bearer $OPERATOR_TOKEN" \05 -H "Content-Type: application/json" \06 -d '{"scopes":[],"ttlHours":720}'07# → { "token": "<INGEST_TOKEN>" }The endpoint
| Field | Type | Description |
|---|---|---|
| {protocol} req | path | Codec name: one of the catalog below (e.g. jt808, modbus, ocpp). |
| Authorization req | header | Bearer token with the ingest role (see above). |
| Content-Type | header | JSON for generic_json/ocpp; binary protocols post raw bytes (no type needed). |
| device | query | Device id for protocols whose wire carries none (modbus, can, jbd, daly, ocpp). Ignored when the wire already identifies the device. |
| body req | raw | The native payload: a JSON document, or the raw protocol bytes (a single message or several concatenated). Max 1 MiB. |
https://api.rootd.cc. The endpoint is idempotent: replaying a frame is safe (de-duplicated on tenant + device + timestamp), so a device can retry freely after a connectivity gap.Device identity
Every reading is attributed to a device. Protocols that carry an id on the wire (a JT808 BCD phone number, a teltonika IMEI, a GB/T VIN, a GT06 login IMEI) use it automatically. Protocols that don't (raw Modbus registers, CAN frames, JBD/Daly BMS, OCPP) take the id from ?device=. Non-UUID ids are mapped to a stable battery-twin id; the raw id is preserved for the fleet map.
Protocol catalog
| Field | Type | Description |
|---|---|---|
| generic_json | generic | Vendor-neutral JSON (object or array). Battery + GPS. Carries its own device id. |
| jt808 | telematics | JT/T 808 GPS trackers (China standard). Binary, 0x7e-framed. Location + EV battery items. |
| gt06 | telematics | GT06 / Concox / Jimi trackers. Binary 0x7878. GPS (device id from login or ?device=). |
| teltonika | telematics | Teltonika Codec 8 / 8E (Europe). Binary AVL. GPS + battery via IO elements. id from ?device=. |
| gbt32960 | telematics | GB/T 32960 China national NEV reporting. Binary ## frame. Battery + GPS + alarms. id = VIN. |
| bms | bms | NW lithium protection board v2.8. Binary NW frame. Battery (voltage/current/temp/SoC/cells). |
| modbus | bms | Modbus RTU (fn 0x03). Universal BMS/charger fieldbus. Battery via register map. id from ?device=. |
| jbd | bms | JBD / Xiaoxiang smart BMS. Binary 0xDD…0x77. Battery. id from ?device=. |
| daly | bms | Daly smart BMS. Binary 13-byte frames. Battery. id from ?device=. |
| can | bus | Raw CAN frames (J1939 / CiA-454 via signal map). Battery. id from ?device=. |
| ocpp | infra | OCPP 1.6 MeterValues from charge/swap cabinets. JSON. Battery/station. id from ?device= + connectorId. |
Examples
generic_json: the simplest path; carries battery and GPS in one JSON document:
01curl -X POST https://api.rootd.cc/v1/ingest/generic_json \02 -H "Authorization: Bearer $INGEST_TOKEN" \03 -H "Content-Type: application/json" \04 -d '{"device":"BIKE-001","timestamp":"2026-06-24T09:00:00Z",05 "v":63.2,"i":-4.5,"temp":29,"soc":0.71,06 "lat":-1.286,"lon":36.817,"speed":18,"heading":75,"altitude":1680}'07# → 202 {"acceptedReadings":1,"acceptedLocations":1}Binary protocols: post the raw wire bytes (here, a JT808 0x0200 report):
01# Binary protocols (jt808, gt06, teltonika, gbt32960, bms, modbus, jbd, daly, can)02# post the raw wire bytes. Send hex through xxd, stream with --data-binary:03echo '7e0200002e8612345678900001...7e' | xxd -r -p | \04 curl -X POST https://api.rootd.cc/v1/ingest/jt808 \05 -H "Authorization: Bearer $INGEST_TOKEN" \06 --data-binary @-07# → 202 {"acceptedReadings":1,"acceptedLocations":1}Identity-less wires: supply the device with ?device= (here, Modbus):
01# Protocols whose wire carries no device id (modbus, can, jbd, daly, ocpp)02# take it from the ?device= query param:03echo '4e57002a60300002...68...' | xxd -r -p | \04 curl -X POST 'https://api.rootd.cc/v1/ingest/modbus?device=PACK-7' \05 -H "Authorization: Bearer $INGEST_TOKEN" --data-binary @-OCPP: charge/swap cabinets post MeterValues:
01curl -X POST 'https://api.rootd.cc/v1/ingest/ocpp?device=CP-001' \02 -H "Authorization: Bearer $INGEST_TOKEN" -H "Content-Type: application/json" \03 -d '[2,"uid","MeterValues",{"connectorId":1,"meterValue":[{04 "timestamp":"2026-06-24T09:00:00Z","sampledValue":[05 {"value":"63.5","measurand":"Voltage","unit":"V"},06 {"value":"80","measurand":"SoC","unit":"Percent"}]}]}]'Responses & errors
A successful POST returns 202 Accepted with the counts decoded from the payload. Control/heartbeat frames are accepted with zero counts.
01{ "acceptedReadings": 1, "acceptedLocations": 1 }| Field | Type | Description |
|---|---|---|
| 202 | Accepted | Decoded and persisted (or queued). |
| 401 | Unauthorized | Missing/invalid bearer token. |
| 403 | Forbidden | Token lacks the ingest role. |
| 404 | Not Found | Unknown {protocol} or unknown tenant. |
| 413 | Payload Too Large | Body over 1 MiB. |
| 422 | Unprocessable | Decode/validation failed: the payload is dead-lettered for inspection. |
| 429 | Too Many Requests | Per-tenant rate limit; honor Retry-After. |
| 503 | Unavailable | Downstream temporarily unhealthy; retry. |
Bidirectional commands
Every codec in the catalog above is paired with an encoder: ingest decodes wire bytes into the twin, the encoder turns operator-issued commands back into the same wire format. One operator-side shape, regardless of whether the device speaks JT808 binary, OCPP JSON-RPC, or anything in between.
device_commands queue with the state machine pending → dispatched → acked | failed | expired. Delivery happens via HTTP response piggyback on the device's next ingest call, an explicit HTTP pull (GET /v1/devices/{id}/commands/pending), a persistent TCP push for JT808/GT06/Teltonika, an MQTT broker on rootd/{tenant}/devices/{id}/cmd, or a full-duplex OCPP WebSocket on /ocpp/{charge_point_id}. The device picks its transport; the operator picks the command.1. Enqueue. The encoder dry-runs synchronously, so an unsupported command 422s before it queues:
01curl -X POST https://api.rootd.cc/v1/devices/BIKE-001/commands \02 -H "Authorization: Bearer $OPERATOR_TOKEN" \03 -H "Content-Type: application/json" \04 -d '{"protocol":"jt808","kind":"reboot","expires_in_s":300}'05# → 201 { "id": "<CMD>", "status": "pending" }| Field | Type | Description |
|---|---|---|
| protocol req | body | Codec name from the catalog (jt808, ocpp, bms, generic_json, …). |
| kind req | body | Normalized command kind: reboot, unlock, firmware_update, remote_start_transaction, set_current_limit, … |
| payload | body | Kind-specific parameters. For firmware_update, pass a FirmwareManifest from POST /v1/firmware. |
| expires_in_s | body | Auto-flip from pending / dispatched to expired after N seconds. 0 = no expiry. |
| battery_id | body | Optional twin id when the command targets one pack inside a multi-battery device. |
2. Deliver. The 202 response to the device's next POST /v1/ingest/{protocol} gains an optional commands array with the encoded bytes (base64) and a delivery token. A persistent-socket device receives the same bytes on the socket within a tick.
3. Ack. The device echoes the delivery token; stale tokens return 404 (replay-safe).
01# Device echoes the delivery_token it received with the encoded frame.02curl -X PATCH https://api.rootd.cc/v1/devices/BIKE-001/commands/<CMD>/ack \03 -H "Authorization: Bearer $INGEST_TOKEN" \04 -H "Content-Type: application/json" \05 -d '{"delivery_token":"<TOKEN>","result":{"status":"ok"}}'06# → 200 { "status": "acked" } (replays of a stale token return 404)device_command_acls is unrestricted. The moment you write one grant, the device becomes deny-by-default: only that (subject, kind) pair (and any other rows you add) can enqueue against it. Useful for high-blast-radius commands like firmware updates. Every grant / revoke is audited.POST /v1/firmware opens a chunked upload; appended chunks are sha256-stamped, finalize verifies the assembled image's sha256 + an Ed25519 signature, then you enqueue a firmware_update command and devices pull manifest + chunks via their existing ingest token. Resume after a crash is "fetch chunk N again".Other protocols
Don't see your device? Three paths keep you covered:
- Cheap GPS trackers (Queclink, Ruptela, Meitrack, H02, TK103…): bridge them through an open tracker server into
generic_json. - OEM clouds (NIU, Yadea, Super Soco, Bosch eBike, Gogoro): forward their REST/webhook payloads to
generic_json. - Custom wire format: add a codec via the protocol SDK. Implement one
Decodefunction and register it. See the ingest reference.