Skip to main content
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 under data, alongside a meta block with requestId, environment, timestamp, and latencyMs. A list response carries {items, pagination} under data. The shape never varies.
In code, unwrap before reading:
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. An EV charger splits the same way, with activeControlMode playing the battery’s currentMode. isCharging says whether energy is moving; activeControlMode says which regime put it that way. A charger sitting still under auto.charge_tariff because the price is high reads isCharging: false, activeControlMode: "auto.charge_tariff". One a driver stopped reads activeControlMode: "idle". Same stillness, opposite meaning, and only the second is something to tell the driver about. notChargingReason answers the follow-up when a car is plugged in and not drawing: vehicle (the car declined or has finished), charger (the charger is limiting or curtailing), authorization (waiting on an RFID card or app approval), schedule (the charger’s own program vetoes charging right now), or unknown (paused, cause not given). Only authorization is something the driver can act on directly.

Optional readings are absent, never zero

An EV charger’s sessionEnergy, phases, voltage, notChargingReason, and activeControlMode are presence-based: a key is missing because this charger does not report that reading, never because the reading happens to be zero. A single-phase charger reporting phases: 1 and a charger that does not report phases at all are different facts, and the response keeps them apart. Check for the key before reading the value. notChargingReason draws that line twice. Its absence means the charger said nothing about why it is paused; unknown means the charger reported a pause and would not name the cause. Do not collapse the two. status: "discharging" is not a charging state. A charger exporting from the car back to the home or the grid reports discharging with a negative currentPower and isCharging: false. Sum currentPower across chargers as a signed number, or an exporting charger will read as load. 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 auto.schedule command.

Three-tier retrieval (live)

In live, reads pass through three tiers in order. Sandbox skips this entirely; see sandbox versus live below.
Cache and live both report 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. A fallback reading can additionally carry metadata.degraded: true. This is narrower than the fallback source on its own: it means Amps has a protective circuit open for this OEM and is deliberately serving last-known state rather than calling the device, not that a single live call happened to fail. The flag is present only when true, so a normal reading omits it entirely. Read it as a signal that the OEM is under active protection, so a retry keeps returning last-known until metadata.source reports live again. 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. A normal request prefers a fresher expedited entry when one exists. Both tiers are checked; whichever is freshest returns. An expedited request goes live on miss; it does not fall back to a 15-minute-old normal cache, because the explicit signal was “I want fresh data”. In live, the cache window is time-bounded only. A push completing does not refresh a stale read for free, so polling the device immediately after a write may return the same view you read before. Two ways to handle this in live, depending on which way you want the trade-off:
  • Subscribe to push.completed for 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.
In sandbox these caveats do not apply: the simulation reflects your latest command on the next read. The cache caveats only exist on the live path. See sandbox versus live.

Transient versus permanent errors

OEM errors are classified: Permanent errors mean the device is definitively unknown for this request, and stale fallback data would mislead you. Transient errors mean the OEM is briefly unavailable, so last-known state is the best answer Amps can give. 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

The state 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. Solar inverters and vehicles are read-only. They report state but take no imperative commands, so they publish no push endpoint and carry no commands map. 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. Push charge 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 live GET /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, and metadata.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 to push.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.

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.
For a read-then-push walkthrough, see the cookbook: schedule an overnight charge.