Twin API
Read the normalized twin and everything derived from it: pack health, rider projections, and station forecasts. All endpoints are read-only, tenant-scoped, and audited.
Overview
The Twin API is how your apps consume Rootd. It never mutates batteries or runs operations; it returns the twin and the intelligence on top of it, for your systems to act on. After you ingest a reading, these are the endpoints that prove it landed and surface what Rootd derived from it.
Base URL: https://api.rootd.cc. All responses are JSON with camelCase fields.
Auth & tenancy
Authenticate with the same bearer token you use for ingest (or any operator/rider/financier session token). The token is bound to one tenant, and that scopes every query: there is no cross-tenant read path and no tenant header to pass.
01# Tenant is carried by the token; there is no tenant header.02curl https://api.rootd.cc/v1/twins \03 -H "Authorization: Bearer $ROOTD_TOKEN"operator role; the rider view requires rider; the loan views require financier. A token without the role gets 403. A built-in signup carries all three.List twins
Returns every battery twin in your tenant, each with derived health and its latest reading (null until the pack has telemetry). State-of-charge is intentionally not a stored field; the response carries raw socRaw and soh so you derive an estimate on read.
01GET /v1/twins02{ "twins": [ {03 "batteryId": "BIKE-001",04 "vendor": "vendor_a",05 "chemistry": "nmc",06 "soh": 90.4,07 "cycleCount": 412,08 "status": "active",09 "connectivity": "online",10 "latest": { "voltage": 53.12, "current": -8.1,11 "temperature": 31, "socRaw": 0.64,12 "ts": "2026-06-24T08:14:02Z" }13} ] }| Field | Type | Description |
|---|---|---|
| batteryId | string | Stable twin id for the pack. |
| soh | number | State-of-health, derived from charge/discharge history. |
| connectivity | enum | Modeled state: online · stale · offline. |
| status | string | Lifecycle status (e.g. active, retired). |
| cycleCount | number | Reported cycle count. |
| latest | object | null | Latest reading: voltage (V), current (A), temperature (°C), socRaw (0..1), ts. |
soh + the latest reading. Treat it as guidance that moves with each reading, never as a peer of soh.Twin history
Returns the hourly telemetry rollups for one twin as {"hourly":[…]}. A twin with no telemetry yet returns an empty array, never a 404, because "no readings" is a valid state.
Rider view
The rider projection: earnings retained, current pack, and a suggested next swap that minimizes travel plus wait. This is a recommendation; your app decides whether and how to surface it.
| Field | Type | Description |
|---|---|---|
| earnings | number | Earnings retained after swap friction: the north-star metric. |
| currentBatteryId | string | Pack the rider is currently on. |
| nextSwapRecommendation | object | Suggested swap window, station, and modeled friction saved. |
| batteries | array | Owned/assigned packs with soh and a computed soc (derived, not stored). |
Read a specific rider with GET /v1/rider/{id}/view.
Station forecast
Projects charged-pack supply per station over the forecast horizon and attaches a peak- smoothing recommendation. Pass ?recompute=true to force a fresh forecast before the response.
01GET /v1/operator/stations/kawangware_hub02{03 "stationId": "kawangware_hub",04 "forecast": [05 { "ts": "2026-06-24T09:00:00Z", "chargedPacks": 82 },06 { "ts": "2026-06-25T02:00:00Z", "chargedPacks": 19 }07 ],08 "recommendation": { "signal": "reroute", "reason": "dry risk 02:00" },09 "asOfTs": "2026-06-24T08:50:00Z"10}| Field | Type | Description |
|---|---|---|
| forecast | array | Per-slot projection: ts + chargedPacks with a confidence interval. |
| recommendation | object | signal (e.g. reroute / hold), reason, and an optional time window. |
| asOfTs | string | When the forecast was computed (UTC). |
| staleSlots | number | Count of horizon slots backed by stale inputs. |
Compare forecast against realized inventory with GET /v1/operator/stations/{id}/accuracy.