GET /battery/{deviceId} returns the device’s current state, its capability declaration, and metadata identifying where the data came from. The retrieval path is identical across device types; only the state schema and command set change.
The response envelope
Every response is wrapped: a successful read carries the device underdata, alongside a meta block with requestId, environment, timestamp, and latencyMs. A list response carries {items, pagination} under data. The shape never varies.
state is the live snapshot. commands and settings are computed from the device’s declared capabilities at read time and merged with live values. See capabilities for the full surface.
Reading state: what versus what was asked
state.status is what the device is doing right now: a battery’s status is one of charging, discharging, idle, or standby; an HVAC state.active flag answers the same question for a thermostat (heating or cooling). state.currentMode (battery) is the standing command the device is operating under. The two can split cleanly: a charge whose target has already been met reads status: "idle", currentMode: "charge", chargeRate: 0. The command stands, the floor of the requested action is satisfied, no energy is moving. Push a higher target to see it charge again.
lastAction and currentSchedule let one read answer “what did Amps last tell this device to do, and what governs it”, so you can render a device card in a single call. Both are summaries with a link, not full copies: follow lastAction.links.self to GET /actions/{actionId} for the complete record (parameters, full result, all timestamps), and the device’s own actions all appear in GET /actions. The summary carries the canonical command, the lifecycle state, createdAt, updatedAt (the most recent state transition), and errorCode/errorMessage (populated on a failed action, null otherwise) — enough to render the card without a second call. lastAction.command is the same canonical verb you sent, and always matches the full record. Reading lastAction against the live state is also how you catch a third-party override: when the device is doing something other than what Amps last commanded, something else moved it. lastAction is null until the device has its first action, and a just-pushed action may take a moment to appear here; if you have just written and need the up-to-date lifecycle state immediately, fetch the action by id at links.self.
currentSchedule is the schedule governing the device. It is null for now and arrives with the scheduler: the GET /schedules and GET /{type}/{id}/schedule routes return 501 until then. Following a schedule already works today through the HVAC follow_schedule command.
Three-tier retrieval (live)
In live, reads pass through three tiers in order. Sandbox skips this entirely; see sandbox versus live below.| Tier | metadata.source | When |
|---|---|---|
| Cache | cache | A recent read is available. |
| Live | live | No recent read, or expedited request. |
| Fallback | fallback | Live call fails with a transient error. |
sync.available: true. Fallback reports sync.available: false so you know the data is not authoritative for the current request. The metadata.source field names the tier explicitly.
In sandbox, none of the above applies. Sandbox devices are simulated, not physical hardware, so reads always carry metadata.source: "projection" and never report cache, live, or fallback. The reported state reflects the commands you have sent, so the next read after a push is consistent with it. See sandbox versus live.
Cache modes (live): normal and expedited
In live, two read tempos exist. They control how close to real time the response sits versus how often the OEM gets called.| Mode | Freshness | When to use |
|---|---|---|
| Normal | Up to 15 minutes old | Default. Covers dashboard reads, periodic checks. |
| Expedited | Up to 1 minute old | Triggered by ?expedite=true. Forces near-fresh data. |
- Subscribe to
push.completedfor the action and react to that, instead of polling the device read. - If you need a fresh read in live right now, call with
?expedite=true.
Transient versus permanent errors
OEM errors are classified:| Class | Codes | Behaviour |
|---|---|---|
| Transient | NETWORK_ERROR, RATE_LIMITED, SERVICE_UNAVAILABLE, TIMEOUT, UNKNOWN_ERROR | Fall back to last-known stored state. |
| Permanent | DEVICE_NOT_FOUND, DEVICE_OFFLINE | Return to caller. No fallback. |
metadata.source: 'fallback' plus sync.available: false make the difference visible.
If no data exists at any tier (empty cache, failing OEM call, no last-known record), the API returns 404 DEVICE_NOT_FOUND.
What is live versus cached
Thestate object is the live or cached device state. The commands and settings objects merge capability declarations with live values: a setting’s bounds come from its declared capabilities, its current value comes from the device. Capability declarations themselves take effect on the next read; there is no separate cache invalidation for capability data.
You see canonical names only; OEM-native identifiers never appear in the response.
Polling cadence and OEM rate limits (live)
In live, per-OEM rate limits are honoured automatically. A burst of expedited reads against the same device coalesces; the platform enforces a minimum gap, so short read intervals paired with high volumes will not inadvertently flood the OEM. For state changes that require sub-minute reaction, prefer webhooks over polling.push.completed and push.failed events deliver to your registered endpoint with retry and signature verification built in. Polling is the right default for dashboard-style displays, where the visible freshness window is acceptable.
In sandbox, no OEM is being called and no rate limits apply. Read as often as your test loop needs.
Per-device-type fields
Each device type exposes its own state schema. The shape is identical across every supported device of the same type; differences in OEM physics (does this battery report a cell-temperature breakdown? does this inverter expose per-string voltages?) surface as nullable fields when the OEM does not populate them.| Device type | Key state fields |
|---|---|
| Battery | status (charging / discharging / idle / standby), level, capacity, chargeRate, dischargeLimit, currentMode (the standing command in canonical vocabulary, e.g. charge, auto.balanced) |
| EV charger | status, isConnected, isCharging, currentPower, maxCurrent, powerRateLimit |
| HVAC | temperature, active (currently heating or cooling), heatSetpoint, coolSetpoint, mode (heat / cool / auto / off), holdType (permanent if a manual mode is overriding the device’s schedule, follow_schedule if it is honouring its programmed schedule) |
| Solar inverter | status, currentPower, producing, energyTotal |
| Vehicle | batteryLevel, range, plugged, charging, fullyCharged (the battery has reached its configured chargeLimit), batteryCapacity (usable kWh), chargeLimit (configured upper-bound percentage), chargeRate, chargeTimeRemaining (minutes to reach chargeLimit), maxCurrent (amps the vehicle will accept) |
POST /solar-inverter/{deviceId} or POST /vehicle/{deviceId} returns 422 DIRECT_ACTION_UNSUPPORTED. Use them for monitoring, dashboards, and fleet-wide reads.
Listing devices
GET /{deviceType} returns a paginated list of devices your API key can access in the current environment. Each entry carries its last known state, and the same lastAction + currentSchedule summaries as the single read, so a fleet view can show what every device is doing without a request per device. Filter by userId to scope to one end-user; paginate via limit (1-50, default 10) and offset (default 0). The list view uses cached state; for fresh data on a single device, hit the device-id endpoint with ?expedite=true.
Sandbox versus live
Sandbox and live share the response shape, the canonical vocabulary, auth, and access control. They differ on what sits behind the read.Sandbox: the simulation reflects your latest command
In sandbox, the canonical actions you push are the device. Pushcharge and the next read returns status: "charging" with level climbing toward the target. Push idle and the next read returns idle. Push nothing and the device drifts on its baseline (a solar curve at midday, a battery cycling on the household pattern). Reads and pushes against the same sandbox device stay consistent without polling intervals or cache waits: sandbox devices are simulated, so the next read after a push reflects that command. The read carries metadata.source: "projection" to mark the state as simulated rather than from real hardware.
Because each read reflects the simulation’s current state, a value still in motion advances between two reads taken moments apart: a charging battery’s level might read 51 then 52. Compare a read against the command you sent, not against an exact earlier number.
Windowed commands behave correctly over time. A command carrying both start and end applies across its window, and the simulated state reverts to a hold once end passes: a read inside the window shows it active, a read afterward shows the device idle, holding whatever level or temperature the window left behind.
In sandbox state and lastAction always agree: the read reflects the action you just sent. The cache caveats described above are live-only; they do not apply to sandbox reads.
Live: the read reflects whatever the OEM reports
A liveGET /battery/{id} reflects whatever the real OEM reports for the device on the other end, which may differ from the last command Amps sent (a homeowner toggled the panel, the OEM denied the write silently, the device is rate-limited). The lastAction summary against the live state lets you see when those diverge. Cache modes, the three-tier retrieval path, and the ?expedite=true query are all properties of the live read.
See auth and environments for how the API key prefix routes you between them.
Frequently asked questions
How fresh is the data returned by GET /battery/?
In sandbox, the next read after a push reflects that command right away; there are no cache windows to wait through, andmetadata.source is always projection. In live, the default read can be up to 15 minutes old; append ?expedite=true to drop to near-fresh (up to 1 minute old) at the cost of an OEM round trip. The metadata.source field names the tier the live response came from (cache, live, or fallback).
What is the difference between live data and cached data?
These tiers apply on live only. Live data (metadata.source: "live") is fetched from the OEM at request time. Cached data (source: "cache") is served from a previous fetch and may be older than the previous read. Both indicate sync.available: true because the data is authoritative for what the device most recently reported. Fallback data (source: "fallback"), served from the last-known stored state when a fresh OEM call fails with a transient error, indicates sync.available: false so you know the reading is not fresh. Sandbox device reads sit outside this scheme entirely and carry source: "projection" because sandbox devices are simulated, not physical hardware.
How do I detect when a device is offline?
The platform classifies OEM errors as transient or permanent.DEVICE_OFFLINE is permanent and returns to the caller without falling back to stale data; the response has the standard error envelope and a 4xx-class status. Transient errors (NETWORK_ERROR, RATE_LIMITED, SERVICE_UNAVAILABLE, TIMEOUT) fall back to last-known and set sync.available: false. Offline detection is a property of the live read path; in sandbox the simulator is always reachable.
After a push completes in live, why might the next read still look stale?
In live, reads are served from a cache by default and a push completing does not refresh that cache on its own. Either subscribe topush.completed for the action and react to that, or call the read with ?expedite=true when freshness matters more than the OEM rate limit. In sandbox, this caveat does not apply: the next read reflects the latest command.
Why does GET /battery (the list endpoint) not show fresh live data?
Listing devices returns up to 50 entries per page; calling the OEM API for each device on every list request would multiply per-OEM load by the number of devices. The list view in live uses cached state. For fresh data on a single device, fetch the device-id endpoint with?expedite=true. The list is shaped for dashboards and inventories; the per-device read is shaped for control loops. In sandbox, the list reflects each device’s most recent command alongside its drift baseline, the same as the single-device read.
Related concepts
Capabilities
The
commands and settings objects on the response.Webhooks
State-change notifications without polling.
Auth and Environments
Sandbox versus live, environment routing.
Error Envelope
DEVICE_NOT_FOUND, transient versus permanent.