# Cancel a scheduled action Source: https://docs.amps.ai/api-reference/actions/cancel-a-scheduled-action /openapi.json post /actions/{actionId}/cancel Cancel a deferred action that is still in `scheduled` state. Actions in any other state (`acknowledged`, `completed`, `failed`, `cancelled`) cannot be cancelled and return 409. # Get action status Source: https://docs.amps.ai/api-reference/actions/get-action-status /openapi.json get /actions/{actionId} Retrieve the current state of any device action for polling. The action lifecycle progresses `scheduled` (deferred only) -> `acknowledged` -> `completed` | `failed`, with `cancelled` reachable from `scheduled`. The `deviceType` field discriminates the device family and the `command`/`parameters` shape. # List actions Source: https://docs.amps.ai/api-reference/actions/list-actions /openapi.json get /actions Returns a paginated list of the actions the API key can access, across every device type, newest first. Each item is the same polymorphic action shape as `GET /actions/{actionId}`; the `deviceType` field discriminates the device family. Filter by `deviceId`, lifecycle `state`, device `type`, or end-user `userId`. Useful for finding scheduled actions to cancel or completed actions to inspect across a whole fleet. # Clear battery schedule Source: https://docs.amps.ai/api-reference/battery/clear-battery-schedule /openapi.json delete /battery/{deviceId}/schedule Coming soon. Clear the schedule on this battery. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get battery schedule Source: https://docs.amps.ai/api-reference/battery/get-battery-schedule /openapi.json get /battery/{deviceId}/schedule Coming soon. Read the schedule set on this battery. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get battery state Source: https://docs.amps.ai/api-reference/battery/get-battery-state /openapi.json get /battery/{deviceId} Retrieve the current state and capabilities of a battery device. # List battery devices Source: https://docs.amps.ai/api-reference/battery/list-battery-devices /openapi.json get /battery Returns a paginated list of battery devices the API key can access, each with the last captured `state` and the canonical `commands` map. Live state is not pulled per call. Use `GET /battery/{deviceId}` for an authoritative read. # Push battery action Source: https://docs.amps.ai/api-reference/battery/push-battery-action /openapi.json post /battery/{deviceId} Send a command to a battery device. Immediate actions fire as soon as the platform dispatches them; deferred actions (with `start`) fire at the requested time. The action is processed asynchronously and a webhook is sent on completion. # Set battery schedule Source: https://docs.amps.ai/api-reference/battery/set-battery-schedule /openapi.json put /battery/{deviceId}/schedule Coming soon. Set or replace the schedule on this battery. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Update battery device settings Source: https://docs.amps.ai/api-reference/battery/update-battery-device-settings /openapi.json post /battery/{deviceId}/settings Write one or more writable settings on the device. Sparse: send only the settings you want to change. Read-only keys (e.g. `scheduler_enabled`) are rejected with 422 `DEVICE_NOT_CAPABLE`. # Clear EV Charger schedule Source: https://docs.amps.ai/api-reference/ev-charger/clear-ev-charger-schedule /openapi.json delete /ev-charger/{deviceId}/schedule Coming soon. Clear the schedule on this EV charger. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get EV Charger charging sessions Source: https://docs.amps.ai/api-reference/ev-charger/get-ev-charger-charging-sessions /openapi.json get /ev-charger/{deviceId}/sessions **Session history depends on the charger.** Read `sessions` on `GET /ev-charger/{deviceId}` first: `false` means the manufacturer records none and this endpoint answers 422 `DEVICE_NOT_CAPABLE`. Returns this charger's charging sessions, newest first. A session is one plug-in-to-completion episode; `measurement` states how `energyDelivered` was arrived at, so a caller billing against the figure can tell a metered reading from an integrated estimate. No `measurement` value is a settlement-grade guarantee. Paged with the usual `limit`/`offset`. # Get EV Charger schedule Source: https://docs.amps.ai/api-reference/ev-charger/get-ev-charger-schedule /openapi.json get /ev-charger/{deviceId}/schedule Coming soon. Read the schedule set on this EV charger. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get EV Charger state Source: https://docs.amps.ai/api-reference/ev-charger/get-ev-charger-state /openapi.json get /ev-charger/{deviceId} Retrieve the current state of an EV Charger device including charging status, power, and connection state # List EV Charger devices Source: https://docs.amps.ai/api-reference/ev-charger/list-ev-charger-devices /openapi.json get /ev-charger Get a paginated list of EV Charger devices with their last pull state # Push EV Charger action Source: https://docs.amps.ai/api-reference/ev-charger/push-ev-charger-action /openapi.json post /ev-charger/{deviceId} Send a command to an EV Charger device. The action is processed asynchronously and a webhook is sent on completion. # Set EV Charger schedule Source: https://docs.amps.ai/api-reference/ev-charger/set-ev-charger-schedule /openapi.json put /ev-charger/{deviceId}/schedule Coming soon. Set or replace the schedule on this EV charger. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Update EV Charger device settings Source: https://docs.amps.ai/api-reference/ev-charger/update-ev-charger-device-settings /openapi.json post /ev-charger/{deviceId}/settings Write one or more writable settings on the device. Sparse: send only the settings you want to change. Unknown or read-only keys are rejected with 422. # Clear HVAC schedule Source: https://docs.amps.ai/api-reference/hvac/clear-hvac-schedule /openapi.json delete /hvac/{deviceId}/schedule Coming soon. Clear the schedule on this HVAC device. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get HVAC schedule Source: https://docs.amps.ai/api-reference/hvac/get-hvac-schedule /openapi.json get /hvac/{deviceId}/schedule Coming soon. Read the schedule set on this HVAC device. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get HVAC state Source: https://docs.amps.ai/api-reference/hvac/get-hvac-state /openapi.json get /hvac/{deviceId} Retrieve the current state of an HVAC device including mode, temperature, and humidity # List HVAC devices Source: https://docs.amps.ai/api-reference/hvac/list-hvac-devices /openapi.json get /hvac Get a paginated list of HVAC devices with their last pull state # Push HVAC action Source: https://docs.amps.ai/api-reference/hvac/push-hvac-action /openapi.json post /hvac/{deviceId} Send a command to an HVAC device. The action is processed asynchronously and a webhook is sent on completion. # Set HVAC schedule Source: https://docs.amps.ai/api-reference/hvac/set-hvac-schedule /openapi.json put /hvac/{deviceId}/schedule Coming soon. Set or replace the schedule on this HVAC device. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # API Reference Source: https://docs.amps.ai/api-reference/introduction Base URL, authentication, and response format for the Amps REST API. Endpoints are grouped by device type. Each type has a list route and a read route. Battery, EV charger, and HVAC add a push route. Battery and EV charger add a settings route, and EV chargers list charging sessions. ## Base URL ``` https://api.amps.ai ``` Sandbox and live share this host. Your API key selects the environment. ## Authentication Send your API key in the `x-api-key` header. See [Authentication](/guides/authentication). ```bash theme={null} curl https://api.amps.ai/battery \ -H "x-api-key: $AMPS_API_KEY" ``` ## Responses Successful responses wrap the result in `data`. Errors return `error.code`, `error.message`, and optional `error.details`. See [Error handling](/guides/error-handling). ```json theme={null} { "success": true, "data": { "id": "device_abc123" }, "meta": { "requestId": "req_8a2Bf3kP", "timestamp": "2026-06-01T10:30:00.000Z", "environment": "sandbox" } } ``` ## Next steps > **[Quickstart](/quickstart)** > > Make your first calls in sandbox. > **[Error codes](/guides/error-handling/error-codes)** > > Every code, its status, and what triggers it. # Get a schedule Source: https://docs.amps.ai/api-reference/schedules/get-a-schedule /openapi.json get /schedules/{scheduleId} Coming soon. Read one schedule by its identifier. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # List schedules Source: https://docs.amps.ai/api-reference/schedules/list-schedules /openapi.json get /schedules Coming soon. List the schedules across every device this API key can reach. The route answers `501 Not Implemented` until first-class schedules ship. **Coming soon.** First-class schedules are on the roadmap. Until they ship, this route answers `501 Not Implemented`. Schedule the work one command at a time instead. Send a push with `start` to run a command later, or with `start` and `end` to run it for a window. Send one push for each time you want something to happen. A device that lists `auto.schedule` in its `commands` map already runs its own stored program when you send that command. # Get Solar Inverter state Source: https://docs.amps.ai/api-reference/solar-inverter/get-solar-inverter-state /openapi.json get /solar-inverter/{deviceId} Retrieve the current state of a solar inverter device including power production, status, and energy totals # List solar inverter devices Source: https://docs.amps.ai/api-reference/solar-inverter/list-solar-inverter-devices /openapi.json get /solar-inverter Get a paginated list of solar inverter devices with their last pull state # Revoke user consent Source: https://docs.amps.ai/api-reference/users/revoke-user-consent /openapi.json delete /users/{userId}/consent Revoke consent for a user. The affected devices stop receiving pulls and pushes. When a revoked link is the last one on a device, and the manufacturer supports it, Amps first hands the device back to its owner at the manufacturer, and revokes the access token there once no other link uses it; if either step fails, the request fails and the links are kept. Pass a `deviceIds` array to scope the revocation; omit it to revoke every device under the user. # Get vehicle state Source: https://docs.amps.ai/api-reference/vehicle/get-vehicle-state /openapi.json get /vehicle/{deviceId} Retrieve the current state of a vehicle device including battery level, range, charging status, and capacity # List vehicle devices Source: https://docs.amps.ai/api-reference/vehicle/list-vehicle-devices /openapi.json get /vehicle Get a paginated list of vehicle devices with their last pull state # Battery Source: https://docs.amps.ai/cookbooks/battery What a battery reports, the commands it accepts, and recipes for common tasks. Read a home battery's charge level and mode, and tell it to charge, discharge, or run itself. Each recipe below covers one task end to end. ## What a battery reports `GET /battery/{id}` returns the battery's `state` alongside the `commands` and `settings` it supports. ```json theme={null} { "success": true, "data": { "state": { "status": "idle", "level": 67, "capacity": 10.4, "chargeRate": 0, "dischargeLimit": 10, "currentMode": "auto.balance" } } } ``` | Field | | | - | - | | `status` | `charging`, `discharging`, `idle`, or `standby`. | | `level` | State of charge, in percent. | | `capacity` | Total capacity, in kWh. | | `chargeRate` | Current rate in kW. Negative while discharging. | | `dischargeLimit` | Discharge limit, in percent. | | `currentMode` | The command the battery is running. Absent when the mode has no canonical name. | See [Pull](/guides/pull) for freshness, caching, and the rest of the device read. ## Commands | Command | What it does | Parameters | Execution | | - | - | - | - | | `charge` | Charge from grid or solar. Stops at `target`. | `target`, `power`, `reserve` | immediate, scheduled, windowed | | `discharge` | Discharge to the home or grid. Stops at `target`. | `target`, `power`, `reserve` | immediate, scheduled, windowed | | `idle` | Pause charging and discharging. | none | immediate, scheduled | | `auto.balance` | Self-consumption: store surplus solar, cover the home's load. | none | immediate, scheduled | | `auto.reserve` | Hold charge in reserve for a grid outage. | none | immediate, scheduled | | `auto.export` | Favour exporting to the grid. | none | immediate, scheduled | `target` and `reserve` are in `percent`, `power` is in `kw`. The table shows the most a battery can accept. Each device lists its own subset, bounds, and execution shapes in `commands`, and anything outside that list returns `422 DEVICE_NOT_CAPABLE`. See [Capabilities](/guides/capabilities). ## Recipes > **[Charge in a cheap-rate window](/cookbooks/battery/charge-in-window)** > > Charge between a `start` and `end`. > **[Start a charge later](/cookbooks/battery/defer-charge)** > > Defer a charge to a future time. > **[Discharge during a peak](/cookbooks/battery/discharge-in-window)** > > Discharge or export in a peak-rate window. > **[Return to self-consumption](/cookbooks/battery/auto-balance)** > > Hand control back with `auto.balance`. > **[Change a setting](/cookbooks/battery/change-settings)** > > Update the discharge floor, charge ceiling, and more. # Return to self-consumption Source: https://docs.amps.ai/cookbooks/battery/auto-balance Put a battery back into its own self-consumption mode with auto.balance. Send `auto.balance` to hand control back to the battery. It stores surplus solar and covers the home's load on its own, using the manufacturer's self-consumption mode. ## 1. Check the battery supports it The battery must list `auto.balance` in `commands`. It takes no parameters and can't be windowed. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "currentMode": "charge" }, "commands": { "auto.balance": { "parameters": {}, "execution": ["immediate", "scheduled"] }, "auto.reserve": { "parameters": {}, "execution": ["immediate", "scheduled"] }, "auto.export": { "parameters": {}, "execution": ["immediate", "scheduled"] } } } } ``` ## 2. Send the command ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "auto.balance" } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_aHjkkbBGW4", "command": "auto.balance", "parameters": null, "state": "acknowledged", "links": { "self": "/actions/action_aHjkkbBGW4" } } } ``` The API returns `202 Accepted`. Add a `start` to switch modes later, for example after a morning charge. Sending a parameter or an `end` returns `422 DEVICE_NOT_CAPABLE`. The other auto modes use the same body: | Command | Use it to | | - | - | | `auto.balance` | Use solar in the home first. | | `auto.reserve` | Keep charge in reserve for a grid outage. | | `auto.export` | Favour exporting to the grid. | ## 3. Confirm Track the command with [action states](/guides/push#action-states) or [webhooks](/guides/webhooks). Read the battery separately to check its reported operating mode. ## Next steps > **[Charge in a cheap-rate window](/cookbooks/battery/charge-in-window)** > > Charge overnight, then balance by day. > **[Push](/guides/push)** > > Action lifecycle and timing. # Change a battery setting Source: https://docs.amps.ai/cookbooks/battery/change-settings Update a battery's discharge floor, charge ceiling, or other persistent settings. Settings are persistent limits, like the lowest charge level the battery will discharge to. Change them with `POST /battery/{id}/settings`. Unlike commands, settings writes are applied in the request and return `200`. ## 1. Check the battery supports it `settings` lists each setting the battery supports, with its current value, unit, and bounds. A setting the battery doesn't report in a usable form is left out. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "settings": { "safety_reserve": { "value": 10, "unit": "percent", "min": 0, "max": 100 }, "discharge_floor": { "value": 20, "unit": "percent", "min": 0, "max": 100 }, "charge_ceiling": { "value": 100, "unit": "percent", "min": 0, "max": 100 }, "export_limit": { "value": 5000, "unit": "watts", "min": 0 }, "max_charge_rate": { "value": 25, "unit": "amps", "min": 0, "max": 1000 }, "max_discharge_rate": { "value": 25, "unit": "amps", "min": 0, "max": 1000 }, "scheduler_enabled": { "value": false } } } } ``` | Setting | Unit | | | - | - | - | | `safety_reserve` | `percent` | Lowest charge level, even in a power cut. | | `discharge_floor` | `percent` | Lowest charge level in normal operation. | | `charge_ceiling` | `percent` | Highest charge level. | | `export_limit` | `watts` | Maximum export to the grid. | | `max_charge_rate` | `amps` | Maximum charge current. | | `max_discharge_rate` | `amps` | Maximum discharge current. | | `scheduler_enabled` | | Read-only. | ## 2. Send the update Send only the settings you want to change. Each value needs the unit shown in step 1. ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123/settings \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "discharge_floor": { "value": 15, "unit": "percent" }, "charge_ceiling": { "value": 95, "unit": "percent" } }' ``` ```json theme={null} { "success": true, "data": { "deviceId": "device_abc123", "updated": ["discharge_floor", "charge_ceiling"] } } ``` | Response | When | | - | - | | `422 DEVICE_NOT_CAPABLE` | The battery doesn't list the setting, or it is read-only. | | `422 SETTING_OUT_OF_RANGE` | The value is outside `min`/`max`, or one the battery can't deliver. | | `422 INVALID_SETTING_UNIT` | The unit doesn't match. | | `409 SCHEDULER_ACTIVE` | A schedule running on the device blocks the write. | See [Error codes](/guides/error-handling/error-codes). ## 3. Confirm `updated` lists the settings that changed. A battery can round a value to the nearest step its hardware supports, so read the battery again to see what it holds. ## Next steps > **[Settings](/guides/capabilities/settings)** > > Settings vs commands, and every setting code. > **[Battery cookbook](/cookbooks/battery)** > > All battery recipes. # Charge in a cheap-rate window Source: https://docs.amps.ai/cookbooks/battery/charge-in-window Charge a battery between a start and end time, such as an overnight off-peak tariff. Charge a battery during an off-peak tariff window by sending `charge` with a `start` and an `end`. Amps runs the charge at `start` and takes it back at `end`, with no further calls from you. ## 1. Check the battery supports it The battery must list `windowed` in `commands.charge.execution`. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 10, "max": 100 }, "power": { "unit": "kw", "min": 0, "max": 5 } }, "execution": ["immediate", "scheduled", "windowed"] } } } } ``` If `windowed` is missing, the push returns `422 DEVICE_NOT_CAPABLE` with the supported shapes in `details.supportedExecution`. ## 2. Send the command Charge to 90% between 00:30 and 04:30, capped at 3 kW. Times are the battery's local time, with no offset. See [Scheduling](/guides/scheduling). ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "start": "2026-11-12T00:30:00", "end": "2026-11-12T04:30:00", "parameters": { "target": { "value": 90, "unit": "percent" }, "power": { "value": 3, "unit": "kw" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_xyVCcLiYU9", "command": "charge", "state": "scheduled", "start": "2026-11-12T00:30:00.000Z", "end": "2026-11-12T04:30:00.000Z", "links": { "self": "/actions/action_xyVCcLiYU9" } } } ``` The API returns `202 Accepted`. The action stays `scheduled` until `start`. To open the window now, leave out `start`: the charge is sent straight away and the action comes back `acknowledged` with its `end`. If the battery already has a pending action, you get a `409`; see [Conflicts](/guides/push/conflicts). ## 3. Confirm Listen for `push.completed` or `push.failed` on your [webhook](/guides/webhooks), or read the action at `links.self`. See [Push](/guides/push) for the action lifecycle. ## Next steps > **[Discharge during a peak](/cookbooks/battery/discharge-in-window)** > > The same window shape, for discharging. > **[Cancel an action](/guides/push/cancelling)** > > Cancel the window before it starts. # Start a charge later Source: https://docs.amps.ai/cookbooks/battery/defer-charge Defer a battery charge to a future time with a start and no end. Start a charge at a future time by sending `charge` with a `start` and no `end`. The charge begins at `start` and continues until you send another command. ## 1. Check the battery supports it The battery must list `scheduled` in `commands.charge.execution`. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 10, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] } } } } ``` If `scheduled` is missing, the push returns `422 DEVICE_NOT_CAPABLE`. ## 2. Send the command Charge to 100% from 02:00, battery local time. `start` also accepts a relative duration such as `"30m"` or `"2h"`. See [Scheduling](/guides/scheduling). ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "start": "2026-11-12T02:00:00", "parameters": { "target": { "value": 100, "unit": "percent" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_z8JuBgdKwH", "command": "charge", "state": "scheduled", "start": "2026-11-12T02:00:00.000Z", "links": { "self": "/actions/action_z8JuBgdKwH" } } } ``` The API returns `202 Accepted`. The action stays `scheduled` until `start`, and you can [cancel it](/guides/push/cancelling) until then. ## 3. Confirm Listen for `push.completed` or `push.failed` on your [webhook](/guides/webhooks), or poll the action: ```bash theme={null} curl https://api.amps.ai/actions/action_z8JuBgdKwH \ -H "x-api-key: $AMPS_API_KEY" ``` ## Next steps > **[Charge in a window](/cookbooks/battery/charge-in-window)** > > Add an `end` so the charge stops on its own. > **[Return to self-consumption](/cookbooks/battery/auto-balance)** > > Hand control back once charging is done. # Discharge during a peak window Source: https://docs.amps.ai/cookbooks/battery/discharge-in-window Discharge or export from a battery between a start and end time, stopping at a minimum charge level. Discharge a battery during a peak-rate window by sending `discharge` with a `start` and an `end`. For `discharge`, `target` is the floor: the battery stops discharging when it reaches that level. ## 1. Check the battery supports it The battery must list `windowed` in `commands.discharge.execution`. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "level": 92 }, "commands": { "discharge": { "parameters": { "target": { "unit": "percent", "min": 10, "max": 100 }, "power": { "unit": "kw", "min": 0, "max": 5 }, "reserve": { "unit": "percent", "min": 0, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] } } } } ``` ## 2. Send the command Discharge at up to 5 kW from 16:00 to 19:00, stopping at 30%. `reserve` sets a second floor that holds while the command runs. ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "discharge", "start": "2026-11-12T16:00:00", "end": "2026-11-12T19:00:00", "parameters": { "target": { "value": 30, "unit": "percent" }, "power": { "value": 5, "unit": "kw" }, "reserve": { "value": 20, "unit": "percent" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_njbw6w8BJM", "command": "discharge", "state": "scheduled", "start": "2026-11-12T16:00:00.000Z", "end": "2026-11-12T19:00:00.000Z", "links": { "self": "/actions/action_njbw6w8BJM" } } } ``` The API returns `202 Accepted`. A value outside a parameter's `min`/`max` returns `422 PARAMETER_OUT_OF_RANGE`. At `end`, Amps takes the command back; send `auto.balance` afterwards if you want the battery to manage itself again. See [Scheduling](/guides/scheduling) for time format and limits. ## 3. Confirm Track the command with [action states](/guides/push#action-states) or [webhooks](/guides/webhooks). Check battery readings during the window for `state.status: "discharging"` and a negative `chargeRate`. ## Next steps > **[Charge in a cheap-rate window](/cookbooks/battery/charge-in-window)** > > Refill the battery overnight. > **[Conflicts](/guides/push/conflicts)** > > Handle a `409` when the battery is busy. # EV charger Source: https://docs.amps.ai/cookbooks/ev-charger What an EV charger reports, what it accepts, and recipes for common tasks. Read an EV charger's session state, start and stop charging, cap how much power it draws, and read its charging history. ## What it reports ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` | Field | | | - | - | | `status` | `available`, `charging`, `discharging`, `scheduled`, `error`, or `offline`. | | `isConnected` | A vehicle is plugged in. | | `isCharging` | The charger is delivering power to the vehicle now. `false` while exporting from it. | | `currentPower` | Power in kW. Positive while charging, negative while exporting. | | `sessionEnergy` | Energy delivered in the current session, in kWh. Resets each plug-in. | | `notChargingReason` | Why a plugged-in vehicle isn't charging, such as `authorization` or `schedule`. | | `activeControlMode` | The command the charger reports it is running under, such as `charge` or `auto.charge_tariff`. | Optional fields are absent when the charger doesn't report them. Beside `state`, `sessions` says whether the charger keeps a charging history. See [Pull](/guides/pull) for the full read shape and freshness. ## What it accepts | Type | Name | Parameters | | - | - | - | | Command | `charge` | Optional rate cap (`power` in `kw` or `current` in `amps`) and stop condition (`target` in `percent` or `energy` in `kwh`). | | Command | `idle` | None. Pauses charging and holds the session. | | Command | `auto.charge_tariff` | None. Charges in the cheapest hours of the tariff set in the manufacturer's app. | | Command | `auto.charge_surplus_only` | None. Charges only from spare solar. | | Command | `auto.charge_surplus_first` | None. Uses spare solar first, then tops up from the grid. | | Setting | `max_charge_rate` | `{ value, unit: "kw" }`. Standing power cap. | | Setting | `max_charge_current` | `{ value, unit: "amps" }`. The same cap in amps. | | Setting | `cable_lock` | `{ value: true }` or `{ value: false }`. | | Setting | `smart_charging` | `{ value: true }` or `{ value: false }`. Turns the charger's own smart schedule on or off. | Each device's `commands` and `settings` show what that charger supports. Which chargers you can control in live depends on the manufacturer: see [Capabilities](/guides/capabilities). ## Recipes > **[Start and stop a session](/cookbooks/ev-charger/start-stop-session)** > > Charge now, pause, or charge inside a time window. > **[Smart charging](/cookbooks/ev-charger/smart-charging)** > > Let the charger pick cheap hours or spare solar. > **[Charge an exact amount](/cookbooks/ev-charger/charge-energy-amount)** > > Stop the session after a set number of kWh. > **[Cap charging power](/cookbooks/ev-charger/cap-power)** > > Set a standing power limit, or cap for a grid event and restore. > **[Why isn't it charging?](/cookbooks/ev-charger/why-not-charging)** > > Read why a plugged-in car takes no power. > **[Read session history](/cookbooks/ev-charger/session-history)** > > List past charging sessions and the energy each one delivered. # Cap charging power Source: https://docs.amps.ai/cookbooks/ev-charger/cap-power Limit how much power an EV charger draws with the max_charge_rate setting, including for a grid event. Charging power is a setting, not a command: write `max_charge_rate` once and the charger keeps it across sessions until you change it. See [Settings](/guides/capabilities/settings) for how settings differ from commands. ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "status": "charging", "currentPower": 11, "powerRateLimit": 11 }, "settings": { "max_charge_rate": { "value": 11, "unit": "kw", "min": 0, "max": 50, "step": 0.1 }, "max_charge_current": { "value": 32, "unit": "amps", "min": 6, "max": 32, "step": 1 } } } } ``` If `settings.max_charge_rate` is missing, the charger doesn't expose a power cap in kW. Stay inside its `min` and `max`, on a multiple of `step`. ## 2. Write the setting Send only the settings you want to change. ```bash theme={null} curl -X POST https://api.amps.ai/ev-charger/device_abc123/settings \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "max_charge_rate": { "value": 7.4, "unit": "kw" } }' ``` ```json theme={null} { "success": true, "data": { "deviceId": "device_abc123", "updated": ["max_charge_rate"] } } ``` Settings return `200`, not `202`, and don't create an action. A value outside the bounds or off the `step` grid returns `422 SETTING_OUT_OF_RANGE`. `max_charge_rate` and `max_charge_current` are the same cap in two units: send one, or you get `422 UNSUPPORTED_SETTING_COMBINATION`. ## 3. Confirm Read the device again: `settings.max_charge_rate.value` shows `7.4`. See [Pull](/guides/pull) for how fresh a read is. To cap a single session instead, send `power` or `current` on the `charge` command. See [Start and stop a session](/cookbooks/ev-charger/start-stop-session). ## Cap for a grid event, then restore A grid operator may require a lower limit for a few hours, such as 4.2 kW. Amps keeps no record of the previous value, so save `settings.max_charge_rate.value` before you write the cap, and write it back when the event ends. ```bash theme={null} # Event starts: you saved 11 from the read in step 1 curl -X POST https://api.amps.ai/ev-charger/device_abc123/settings \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "max_charge_rate": { "value": 4.2, "unit": "kw" } }' # Event ends: restore the saved value curl -X POST https://api.amps.ai/ev-charger/device_abc123/settings \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "max_charge_rate": { "value": 11, "unit": "kw" } }' ``` The cap also applies to sessions that start during the event. Some cars only slow down after a pause in the session, so if the limit is a hard obligation, push `idle` for the event instead. The restore overwrites any change someone else made meanwhile: read the device first if that matters. ## Next steps > **[Start and stop a session](/cookbooks/ev-charger/start-stop-session)** > > Charge now, pause, or charge inside a time window. > **[Settings](/guides/capabilities/settings)** > > Every setting code and how writes work. # Charge an exact amount Source: https://docs.amps.ai/cookbooks/ev-charger/charge-energy-amount Deliver a fixed number of kilowatt-hours with the charge command's energy parameter. Push `charge` with an `energy` amount, such as 20 kWh for a guest or a prepaid top-up, and the charger stops the session when it has delivered it. The charger's own meter enforces the stop, so only chargers that declare `energy` accept it. ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "commands": { "charge": { "parameters": { "power": { "unit": "kw", "min": 1.4, "max": 22, "step": 0.1 }, "energy": { "unit": "kwh", "min": 1, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] } } } } ``` No `energy` key means the charger can't meter the stop. Use `target` in `percent` if it declares that, or push `charge`, watch `state.sessionEnergy`, and push `idle` yourself. ## 2. Send the command Deliver 20 kWh at no more than 7.4 kW: ```bash theme={null} curl -X POST https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "energy": { "value": 20, "unit": "kwh" }, "power": { "value": 7.4, "unit": "kw" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_XFLvi9c3FB", "command": "charge", "parameters": { "energy": { "value": 20, "unit": "kwh" }, "power": { "value": 7.4, "unit": "kw" } }, "state": "acknowledged" } } ``` An amount outside `min` and `max` returns `422 PARAMETER_OUT_OF_RANGE`, with the bounds in `details`. On a charger that doesn't declare `energy` you get `422 DEVICE_NOT_CAPABLE`, and `details.deviceCapabilities.supportedParameters` lists what it does take. ```json theme={null} { "success": false, "error": { "code": "PARAMETER_OUT_OF_RANGE", "message": "One or more parameters are outside the supported range.", "details": { "parameter": "energy", "value": 150, "min": 1, "max": 100, "unit": "kwh" } } } ``` ## 3. Confirm Track command dispatch through [action states](/guides/push#action-states). `completed` does not confirm that charging started or that 20 kWh arrived. Poll the device and watch `state.sessionEnergy` climb. When `isCharging` turns `false` with `sessionEnergy` at the amount, the charger has delivered it. For the finished total, see [Read session history](/cookbooks/ev-charger/session-history). ## Next steps > **[Start and stop a session](/cookbooks/ev-charger/start-stop-session)** > > Charge now, pause, or charge inside a time window. > **[Read session history](/cookbooks/ev-charger/session-history)** > > What each session delivered, and how it was measured. # Read session history Source: https://docs.amps.ai/cookbooks/ev-charger/session-history List an EV charger's past charging sessions and the energy each one delivered. A charging session is one plug-in to unplug. List a charger's sessions, newest first, to show last night's charge or reconcile energy over a month. ## 1. Check the device keeps history ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "sessions": true } } ``` `sessions: false` means the manufacturer records no history. Asking anyway returns `422 DEVICE_NOT_CAPABLE`, every time, so read the flag instead of retrying. ## 2. List sessions ```bash theme={null} curl "https://api.amps.ai/ev-charger/device_abc123/sessions?limit=2" \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "items": [ { "id": "evs_Ht6vB1zQnE5wXpKm7ArJdU", "status": "active", "startedAt": "2026-07-29T22:48:00.000Z", "energyDelivered": { "value": 12.3, "unit": "kwh" }, "measurement": "metered" }, { "id": "evs_9tQ2mK4xPvR8sLdN3bWfYc", "status": "completed", "startedAt": "2026-07-28T22:31:00.000Z", "endedAt": "2026-07-29T04:12:00.000Z", "energyDelivered": { "value": 41.6, "unit": "kwh" }, "measurement": "metered", "endReason": "vehicle_finished" } ], "pagination": { "limit": 2, "offset": 0, "total": 3, "hasMore": true }, "window": { "from": "2026-06-30T09:15:00.000Z", "to": "2026-07-30T09:15:00.000Z" } } } ``` An `active` session has no `endedAt` or `endReason`, and its `energyDelivered` is a running total. Times are UTC, and `id` is stable, so you can store it as a key. | Field | | | - | - | | `measurement` | How `energyDelivered` was measured: `metered` (the charger's energy register), `oem_reported` (the manufacturer's total), or `inferred` (estimated from power readings). None is a billing-grade guarantee. | | `endReason` | `unplugged`, `vehicle_finished`, `stopped`, `power_lost`, `fault`, or `unknown`. Absent when the charger gives no reason. | ## 3. Filter and page | Parameter | | | - | - | | `from`, `to` | ISO 8601 UTC. Returns sessions that overlap the window. `to` defaults to now, `from` to 30 days before `to`. Up to 400 days. | | `status` | `active` or `completed`. | | `limit`, `offset` | `limit` is 1 to 50, default 10. | Pin the window when you page: send the first page's `window.to` as `to` on every later request, so a session that starts mid-walk doesn't shift rows past you. ```bash theme={null} curl "https://api.amps.ai/ev-charger/device_abc123/sessions?limit=50&offset=50&to=2026-07-30T09:15:00.000Z" \ -H "x-api-key: $AMPS_API_KEY" ``` Stop when `pagination.hasMore` is `false`. For energy in the session that's running now, `state.sessionEnergy` on the device read is the live figure. ## Next steps > **[Start and stop a session](/cookbooks/ev-charger/start-stop-session)** > > Charge now, pause, or charge inside a time window. > **[Pull](/guides/pull)** > > Read shape, freshness, and pagination. # Smart charging Source: https://docs.amps.ai/cookbooks/ev-charger/smart-charging Hand charging times to the charger's own price or solar optimiser, and read back which mode it is running. Push an `auto.*` command and the charger picks the hours itself, using the tariff set in the manufacturer's app or what the solar array is producing. Use a [windowed `charge`](/cookbooks/ev-charger/start-stop-session) instead when you want to own the timing. | Command | The charger optimises for | | - | - | | `auto.charge_tariff` | Price. Charges in the cheapest hours of the home's configured tariff. | | `auto.charge_surplus_only` | Zero import. Charges from spare solar and pauses when it runs out, so the car may not fill. | | `auto.charge_surplus_first` | A full car. Uses spare solar first, then tops up from the grid. | ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "commands": { "auto.charge_tariff": { "parameters": {}, "execution": ["immediate", "scheduled"] }, "auto.charge_surplus_first": { "parameters": {}, "execution": ["immediate", "scheduled"] } } } } ``` A mode missing from `commands` returns `422 DEVICE_NOT_CAPABLE`. Modes take no parameters and no `end`: they run until another command replaces them, and either one also returns `422 DEVICE_NOT_CAPABLE`. ## 2. Send the command ```bash theme={null} curl -X POST https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "auto.charge_tariff" } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_esFwvosZG6", "command": "auto.charge_tariff", "parameters": null, "state": "acknowledged" } } ``` Add a `start` to switch modes later, such as `"start": "2026-07-30T18:00:00"` in plant-local time. To leave the mode, push `idle`, `charge`, or another `auto.*`. See [Conflicts](/guides/push/conflicts) if you get a `409`. ## 3. Confirm The mode is armed, and nothing may flow for hours. Read the device to see which mode holds it: ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "status": "scheduled", "isConnected": true, "isCharging": false, "notChargingReason": "schedule", "activeControlMode": "auto.charge_tariff" } } } ``` The charger is waiting for a cheap hour, so show "waiting for off-peak", not "not charging". `activeControlMode` is absent on chargers that don't report it. See [Why isn't it charging?](/cookbooks/ev-charger/why-not-charging) for the other reasons. ## Next steps > **[Start and stop a session](/cookbooks/ev-charger/start-stop-session)** > > Charge inside a window you choose. > **[Why isn't it charging?](/cookbooks/ev-charger/why-not-charging)** > > Read the reason a plugged-in car takes no power. # Start and stop a session Source: https://docs.amps.ai/cookbooks/ev-charger/start-stop-session Start, pause, or window an EV charging session with the charge and idle commands. Start charging with `charge` and pause with `idle`. Add `start` and `end` to charge only inside a time window, such as a cheap overnight rate. ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "status": "available", "isConnected": true, "isCharging": false }, "commands": { "charge": { "parameters": { "power": { "unit": "kw", "min": 1.4, "max": 22, "step": 0.1 }, "energy": { "unit": "kwh", "min": 1, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] }, "idle": { "parameters": {}, "execution": ["immediate", "scheduled"] }, "auto.charge_tariff": { "parameters": {}, "execution": ["immediate", "scheduled"] } }, "conflictStrategies": ["cancel_and_replace", "queue_after"] } } ``` `charge` accepts `windowed`, so it can take both `start` and `end`. This charger caps by `power` and stops by `energy`; it doesn't declare `target`, so sending one returns `422 DEVICE_NOT_CAPABLE`. ## 2. Send the command Charge at up to 7 kW from 00:30 to 04:30, plant-local time: ```bash theme={null} curl -X POST https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "power": { "value": 7, "unit": "kw" } }, "start": "2026-06-02T00:30:00", "end": "2026-06-02T04:30:00" } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_AUR2pdqvgT", "command": "charge", "parameters": { "power": { "value": 7, "unit": "kw" } }, "state": "scheduled", "start": "2026-06-01T23:30:00.000Z", "end": "2026-06-02T03:30:00.000Z", "links": { "self": "/actions/action_AUR2pdqvgT" } } } ``` Omit `start` and `end` to charge now. Send `{ "action": { "command": "idle" } }` to pause. Send `power` or `current`, not both, or you get `422 UNSUPPORTED_PARAMETER_COMBINATION`. See [Scheduling](/guides/scheduling) for time format and what happens at `end`, and [Conflicts](/guides/push/conflicts) if you get a `409`. ## Let the charger pick the time To hand timing to the charger's own price or solar optimiser instead, push an `auto.*` command. See [Smart charging](/cookbooks/ev-charger/smart-charging). ## 3. Confirm Poll `GET /actions/action_AUR2pdqvgT` or listen for webhooks. See [Push](/guides/push) for the action lifecycle. Chargers that report it then read `activeControlMode: "charge"`. If no vehicle is plugged in, an immediate push either comes back with a `vehicle_not_connected` entry in `warnings` (accepted, starts when a car plugs in) or, on chargers that can't arm a charge in advance, `409 VEHICLE_NOT_CONNECTED`. ## Next steps > **[Charge an exact amount](/cookbooks/ev-charger/charge-energy-amount)** > > Stop the session after a set number of kWh. > **[Cancelling](/guides/push/cancelling)** > > Cancel a scheduled session before it starts. > **[Webhooks](/guides/webhooks)** > > Get notified when the session starts. # Why isn't it charging? Source: https://docs.amps.ai/cookbooks/ev-charger/why-not-charging Read why a plugged-in car takes no power, and what to tell the driver. A car is plugged in and nothing flows. One device read usually explains it: `activeControlMode` says which mode holds the charger, and `notChargingReason` says what is stopping the current. ## 1. Read the charger ```bash theme={null} curl https://api.amps.ai/ev-charger/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "status": "available", "isConnected": true, "isCharging": false, "currentPower": 0, "notChargingReason": "authorization" } } } ``` `isConnected: true` with `isCharging: false` means a car is on the cable and taking nothing. Here the charger is waiting for approval, so the driver needs to approve the session, not call an engineer. ## 2. Check the mode first If `activeControlMode` starts with `auto.`, the charger is running a [smart charging](/cookbooks/ev-charger/smart-charging) mode and is waiting for a cheap hour or spare solar. That is the mode working: show "waiting for the right moment", not a fault. ## 3. Read the reason | `notChargingReason` | Meaning | Tell the driver | | - | - | - | | `vehicle` | The car declined the charge or isn't ready. | Check the car. | | `target_reached` | The car reached its own charge limit and stopped. | Charging is finished. | | `charger` | The charger is limiting or curtailing. | Check the charger. | | `load_management` | A site power share is holding the charge. | Wait. It resumes when capacity frees. | | `authorization` | Waiting for an RFID card or app approval. | Approve the session or present the card. | | `schedule` | The charger's own schedule blocks charging now. | Change the schedule in the manufacturer's app. | | `unknown` | The charger reports a pause but not why. | The charge is on hold. | The field is absent while charging, while nothing is plugged in, or when the charger doesn't report reasons. Absent isn't `unknown`: check the key exists before you read it. ```javascript theme={null} const { state } = (await res.json()).data; if (state.isCharging) return "Charging"; if (!state.isConnected) return "Plug the car in"; if (state.activeControlMode?.startsWith("auto.")) return "Waiting for the right moment"; if (!("notChargingReason" in state)) return "Paused"; return state.notChargingReason; ``` ## At push time An immediate `charge` or `auto.*` push can come back with a `warnings` array: `vehicle_not_connected` or `awaiting_authorization`. The command is still accepted and takes effect when the condition clears. No `warnings` means nothing was blocking it when you sent it. ## Next steps > **[Smart charging](/cookbooks/ev-charger/smart-charging)** > > The modes behind an `activeControlMode` that starts with `auto.`. > **[Pull](/guides/pull)** > > Read shape, freshness, and absent fields. # HVAC Source: https://docs.amps.ai/cookbooks/hvac What an HVAC device reports, what it accepts, and recipes for common tasks. Read a thermostat or heat pump's temperature and setpoints, hold it at a temperature, or hand control back to its own schedule. ## What it reports ```bash theme={null} curl https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` | Field | | | - | - | | `temperature` | Current temperature, in °C. | | `active` | The unit is heating or cooling now. | | `heatSetpoint` | Heats below this, in °C. | | `coolSetpoint` | Cools above this, in °C. | | `mode` | `heat`, `cool`, `auto`, or `off`. | | `holdType` | `permanent` when a fixed setpoint overrides the schedule, `schedule` when the device runs its own program. | See [Pull](/guides/pull) for the full read shape and freshness. ## What it accepts | Command | Parameters | | - | - | | `heat` | `target`, in `celsius`. | | `cool` | `target`, in `celsius`. | | `auto.maintain` | `heatSetpoint` and `coolSetpoint`, in `celsius`. Keeps the temperature inside the band (reads back as `mode: "auto"`). | | `idle` | None. Stops heating and cooling (reads back as `mode: "off"`). | | `auto.schedule` | None. Hands control back to the device's own schedule. Immediate only. | HVAC devices have no settings. Each device's `commands` shows its bounds and timing options. Live HVAC control is not open yet: see [Capabilities](/guides/capabilities). ## Recipes > **[Hold a temperature](/cookbooks/hvac/hold-temperature)** > > Override the schedule until you release it. > **[Heat for a window](/cookbooks/hvac/heat-for-a-window)** > > Heat or cool between a start and end time. # Heat for a window Source: https://docs.amps.ai/cookbooks/hvac/heat-for-a-window Heat or cool an HVAC device between a start and end time. Add `start` and `end` to a `heat` or `cool` command to hold a temperature for a fixed window, such as warming the house before people get home. This is a one-off window, not a recurring schedule. ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "commands": { "heat": { "parameters": { "target": { "unit": "celsius", "min": 10, "max": 35 } }, "execution": ["immediate", "scheduled", "windowed"] }, "auto.maintain": { "parameters": { "heatSetpoint": { "unit": "celsius", "min": 10, "max": 35 }, "coolSetpoint": { "unit": "celsius", "min": 10, "max": 35 } }, "execution": ["immediate", "scheduled"] } } } } ``` Only commands with `windowed` in `execution` accept an `end`. Here that's `heat`, not `auto.maintain`. ## 2. Send the command Heat to 21 °C from 17:00 to 22:00, plant-local time: ```bash theme={null} curl -X POST https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "heat", "parameters": { "target": { "value": 21, "unit": "celsius" } }, "start": "2026-06-01T17:00:00", "end": "2026-06-01T22:00:00" } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_watbxVmQ8q", "command": "heat", "state": "scheduled", "start": "2026-06-01T16:00:00.000Z", "end": "2026-06-01T21:00:00.000Z" } } ``` You write plant-local wall-clock time and read back UTC. See [Scheduling](/guides/scheduling) for the time format, limits, and what happens at `end`. ## 3. Confirm The action moves from `scheduled` to `completed` once it runs. Track it on `GET /actions/action_watbxVmQ8q` or with [webhooks](/guides/webhooks). To call it off before `start`, [cancel it](/guides/push/cancelling). ## Next steps > **[Hold a temperature](/cookbooks/hvac/hold-temperature)** > > Override the schedule with no end time. > **[Scheduling](/guides/scheduling)** > > Deferred and windowed commands in detail. # Hold a temperature Source: https://docs.amps.ai/cookbooks/hvac/hold-temperature Override an HVAC device's schedule with a fixed temperature, then release it. Push `heat` or `cool` with a `target` to hold that temperature until you say otherwise. Push `auto.schedule` to release the hold. ## 1. Check the device supports it ```bash theme={null} curl https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "temperature": 18.5, "mode": "heat", "heatSetpoint": 19, "holdType": "schedule" }, "commands": { "heat": { "parameters": { "target": { "unit": "celsius", "min": 10, "max": 35 } }, "execution": ["immediate", "scheduled", "windowed"] }, "auto.schedule": { "parameters": {}, "execution": ["immediate"] } }, "conflictStrategies": ["cancel_and_replace"] } } ``` ## 2. Send the command ```bash theme={null} curl -X POST https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "heat", "parameters": { "target": { "value": 21, "unit": "celsius" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_MXfJmEfsEp", "command": "heat", "parameters": { "target": { "value": 21, "unit": "celsius" } }, "state": "acknowledged" } } ``` To hold a band instead, push `auto.maintain` with `heatSetpoint` and `coolSetpoint`. A new command replaces the hold; if the previous one is still in flight you'll get a `409` (see [Conflicts](/guides/push/conflicts)). ## 3. Confirm Track the command with [action states](/guides/push#action-states). Read the device separately to check `holdType` and `heatSetpoint`. ## Release the hold ```bash theme={null} curl -X POST https://api.amps.ai/hvac/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "auto.schedule" } }' ``` The device reads `holdType: "schedule"` again. `auto.schedule` is immediate only: sending `start` or `end` with it returns `422 DEVICE_NOT_CAPABLE`. ## Next steps > **[Heat for a window](/cookbooks/hvac/heat-for-a-window)** > > Hold a temperature between a start and end time. > **[Webhooks](/guides/webhooks)** > > Get notified when the action completes or fails. # Solar Source: https://docs.amps.ai/cookbooks/solar What a solar inverter reports, and recipes for using its production data. Read how much a solar inverter is producing now and how much it has produced in total. ## What it reports ```bash theme={null} curl https://api.amps.ai/solar-inverter/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` | Field | | | - | - | | `status` | `producing`, `idle`, `night_mode`, `error`, or `offline`. | | `producing` | The inverter is producing power now. | | `currentPower` | AC output now, in kW. | | `energyTotal` | Lifetime production, in kWh. | See [Pull](/guides/pull) for the full read shape and freshness. ## What it accepts Nothing. Solar inverters are read-only: there is no command or settings endpoint for them. See [Capabilities](/guides/capabilities). ## Recipes > **[Monitor production](/cookbooks/solar/monitor-production)** > > Read and poll production for one inverter or a user's fleet. > **[Charge a battery from solar](/cookbooks/solar/charge-battery-from-solar)** > > Watch production and charge a battery when it's high. # Charge a battery from solar Source: https://docs.amps.ai/cookbooks/solar/charge-battery-from-solar Poll solar production and push a battery charge when output is high. Amps doesn't link devices together or act on its own. To charge a battery when the sun is out, poll the inverter yourself and push a `charge` to the battery when production crosses your threshold. ## 1. Check the battery supports it ```bash theme={null} curl https://api.amps.ai/battery/device_bat456 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_bat456", "state": { "status": "idle", "level": 42, "currentMode": "auto.balance" }, "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 0, "max": 100 }, "power": { "unit": "kw", "min": 0, "max": 1000 } }, "execution": ["immediate", "scheduled", "windowed"] } }, "conflictStrategies": ["cancel_and_replace", "queue_after"] } } ``` ## 2. Read production ```bash theme={null} curl https://api.amps.ai/solar-inverter/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "state": { "status": "producing", "currentPower": 3.4 } } } ``` `currentPower` is total output, not surplus. Amps doesn't see household consumption, so subtract your own estimate if you have one. ## 3. Send the charge If output is above your threshold, charge the battery and cap the rate near the output: ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_bat456 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "target": { "value": 90, "unit": "percent" }, "power": { "value": 3, "unit": "kw" } } }, "onConflict": "cancel_and_replace" }' ``` ```json theme={null} { "success": true, "data": { "id": "action_qVEFrQ3kYE", "command": "charge", "state": "acknowledged" } } ``` `cancel_and_replace` replaces whatever the battery was last told to do. See [Conflicts](/guides/push/conflicts). When production drops, push `auto.balance` to hand control back to the battery. ## 4. Confirm Check `GET /actions/action_qVEFrQ3kYE` for `completed`, then repeat from step 2 on your poll interval. See [Push](/guides/push). ## Next steps > **[Monitor production](/cookbooks/solar/monitor-production)** > > Polling and freshness for inverters. > **[Battery cookbook](/cookbooks/battery)** > > More ways to control a battery. # Monitor production Source: https://docs.amps.ai/cookbooks/solar/monitor-production Read a solar inverter's output, poll it, and list every inverter for a user. Read current output from one inverter, or list every inverter linked to one of your users. ## 1. List a user's inverters Filter by the `userId` you used when the device was linked. ```bash theme={null} curl "https://api.amps.ai/solar-inverter?userId=user_abc123" \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "items": [ { "id": "device_abc123", "state": { "status": "producing", "currentPower": 3.2, "energyTotal": 18452.7 } } ], "pagination": { "limit": 10, "offset": 0, "total": 1, "hasMore": false } } } ``` The list shows each device's last known state. Paginate with `limit` (up to 50) and `offset`. ## 2. Read one inverter ```bash theme={null} curl "https://api.amps.ai/solar-inverter/device_abc123?expedite=true" \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "sync": { "available": true, "lastPulledAt": "2026-06-01T12:04:00.000Z" }, "state": { "status": "producing", "producing": true, "currentPower": 3.4, "energyTotal": 18452.9 } } } ``` `?expedite=true` asks for a fresher reading. See [Pull](/guides/pull) for caching and rate limits before you pick a poll interval. ## 3. Work out energy over a period `energyTotal` is a lifetime counter. Store it at the start and end of a period and subtract to get the energy produced in that period. In sandbox, production follows a daily curve in UTC: nothing at night, peaking around midday. ## Next steps > **[Charge a battery from solar](/cookbooks/solar/charge-battery-from-solar)** > > Act on high production. > **[Pull](/guides/pull)** > > Read shape, freshness, and pagination. # Vehicle Source: https://docs.amps.ai/cookbooks/vehicle What vehicle state you can read through Amps. Read an electric vehicle's battery level, range, and charging status. ## What it reports ```bash theme={null} curl https://api.amps.ai/vehicle/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "batteryLevel": 64, "range": 182, "plugged": true, "charging": true, "fullyCharged": false, "batteryCapacity": 75, "chargeLimit": 80, "chargeRate": 7.2, "chargeTimeRemaining": 95, "maxCurrent": 32 } } } ``` | Field | | | - | - | | `batteryLevel` | State of charge, 0–100 percent. | | `range` | Estimated driving range, in miles. | | `plugged` | Plugged in to a charger. | | `charging` | Drawing charge now. | | `fullyCharged` | Reached `chargeLimit`. | | `batteryCapacity` | Usable capacity, in kWh. | | `chargeLimit` | Configured charge limit, in percent. | | `chargeRate` | Charge rate, in kW. Negative when discharging. | | `chargeTimeRemaining` | Minutes until `chargeLimit` is reached. | | `maxCurrent` | Maximum current the vehicle accepts, in amps. | List a user's vehicles with `GET /vehicle?userId=user_abc123`. See [Pull](/guides/pull) for freshness and pagination. In sandbox, vehicles follow a daily pattern in UTC: charging overnight, unplugged and draining during the day. ## What it accepts Nothing yet. Vehicles are read-only: there is no command or settings endpoint for them. To control charging, use the [EV charger](/cookbooks/ev-charger) the car is plugged in to. See [Capabilities](/guides/capabilities). **Coming soon.** Vehicle control, such as starting and stopping charging. ## Next steps > **[Pull](/guides/pull)** > > Read shape, freshness, and pagination. > **[EV charger cookbook](/cookbooks/ev-charger)** > > Control charging from the charger side. # Glossary Source: https://docs.amps.ai/glossary Amps API terms and where each one is explained. | Term | Description | Reference | | - | - | - | | Action | The record created when you push a command. It has an `id` you can track. | [Push](/guides/push) | | Action state | Where an action is in its lifecycle: `scheduled`, `acknowledged`, `completed`, `failed`, or `cancelled`. | [Push](/guides/push) | | `auto.*` commands | Commands that hand control to the device's own logic, such as `auto.balance` for a battery or `auto.schedule` for a thermostat. | [Push](/guides/push) | | Capabilities | The `commands`, `settings`, and `conflictStrategies` a device declares on its read. Build requests from these. | [Capabilities](/guides/capabilities) | | Capability refusal | `422 DEVICE_NOT_CAPABLE` when a device doesn't offer what you asked for, or `503 CAPABILITY_PAUSED` when Amps has paused it. | [Capabilities](/guides/capabilities) | | Charging session | One charge of a vehicle on an EV charger, listed with `GET /ev-charger/{deviceId}/sessions`. | [Session history](/cookbooks/ev-charger/session-history) | | Command | What you ask a device to do, such as `charge`, `idle`, or `heat`. | [Push](/guides/push) | | Conflict strategy | The `onConflict` value that decides what happens when a device already has a pending action: `cancel_and_replace` or `queue_after`. | [Conflicts](/guides/push/conflicts) | | Consent revocation | Removing your access to an end user's devices with `DELETE /users/{userId}/consent`. | [Link UI](/guides/link-ui) | | Envelope | The `{ success, data \| error, meta }` wrapper on every response. | [Error handling](/guides/error-handling) | | Execution | When a command can run: `immediate`, `scheduled`, or `windowed`. Listed per command on the device read. | [Scheduling](/guides/scheduling) | | Expedite | `?expedite=true` on a read, for fresher state from the manufacturer. | [Pull](/guides/pull) | | `externalUserRef` | Your identifier for an end user, passed to Link UI. The same value is the `userId` filter on device lists and consent revocation. | [Link UI](/guides/link-ui) | | Link UI | The hosted page where end users connect their manufacturer accounts. | [Link UI](/guides/link-ui) | | Live | The environment for real devices. Reached with an `sk_live_` key. | [Authentication](/guides/authentication) | | Plant-local time | Wall-clock time at the device's location, with no UTC offset. Used for `start` and `end`. | [Scheduling](/guides/scheduling) | | Quantity | A `{ value, unit }` pair used for every command parameter and setting. | [Push](/guides/push) | | Read-only device | Solar inverters and vehicles. You can read them but not send them commands. | [Capabilities](/guides/capabilities) | | Sandbox | The environment for simulated devices. Reached with an `sk_test_` key. Simulated state reflects the commands you send. | [Authentication](/guides/authentication) | | Setting | A standing configuration value on a device, such as a charge rate limit. Changed with a settings write. | [Settings](/guides/capabilities/settings) | | Webhook | An HTTP request Amps sends you when an action finishes or a device's connection changes. | [Webhooks](/guides/webhooks) | | Window | An action with an `end`. It runs from `start` (or now) until `end`. | [Scheduling](/guides/scheduling) | # AI agents Source: https://docs.amps.ai/guides/ai-agents Give your coding assistant or agent the Amps docs through MCP or llms.txt. Connect your AI tools to these docs so they use real endpoints, field names, and commands instead of guessing. ## Docs MCP server The docs are served as an MCP server at `https://docs.amps.ai/mcp`. It has two read-only tools: one searches the docs, the other reads pages and the OpenAPI spec. **Claude Code** ```bash theme={null} claude mcp add --transport http amps-docs https://docs.amps.ai/mcp ``` **Claude Desktop, Cursor, Windsurf** Add this to the client's MCP config (`claude_desktop_config.json`, `~/.cursor/mcp.json`, or `~/.codeium/windsurf/mcp_config.json`), then restart the client. ```json theme={null} { "mcpServers": { "amps-docs": { "url": "https://docs.amps.ai/mcp" } } } ``` To check it works, ask: "Which commands does an Amps battery accept?" The agent should call the docs tools before answering. ## llms.txt For tools without MCP support, point them at a plain-text version of the docs. | URL | Contains | | - | - | | `https://docs.amps.ai/llms.txt` | An index of every page with a one-line summary. | | `https://docs.amps.ai/llms-full.txt` | The full text of every page. | Any page is also available as Markdown: add `.md` to its URL. ## Execution MCP **Coming soon.** An MCP server that lets agents call the Amps API on your users' behalf. ## Next steps > **[Quickstart](/quickstart)** > > Make your first calls in sandbox. > **[API Reference](/api-reference/introduction)** > > Every endpoint, request, and response. # Authentication Source: https://docs.amps.ai/guides/authentication Create an API key, send it with every request, and choose between sandbox and live. Every request needs an API key. The key also decides whether you reach sandbox or live. ## Get a key Create keys in the [dashboard](https://app.amps.ai) under **API Keys**. A key belongs to the environment the dashboard is switched to when you create it. The full key is shown once, so copy it then. > [!NOTE] > > New to Amps? [Request sandbox access](https://tally.so/r/D4zgZE). ## Send the key Put the key in the `x-api-key` header. ```bash theme={null} curl https://api.amps.ai/battery \ -H "x-api-key: $AMPS_API_KEY" ``` A missing key returns `401 UNAUTHORIZED`. An unrecognised or revoked key returns `401 INVALID_API_KEY`. ## Sandbox and live The prefix tells you which environment a key belongs to. Both use the same host. | Prefix | Environment | Devices | | - | - | - | | `sk_test_` | Sandbox | Simulated. Safe to experiment with. | | `sk_live_` | Live | Your end users' real devices. | Data never crosses environments: a sandbox key can't see live devices, and a live key can't see sandbox devices. Live keys only work once your account is enabled for live. Until then they return `403 LIVE_ACCESS_DISABLED`. To move to production, swap your `sk_test_` key for an `sk_live_` key and drop `sandbox=true` from your [Link UI](/guides/link-ui) URLs. Nothing else changes. ## Check the environment Every authenticated response reports the environment that served it in `meta.environment`. ```json theme={null} { "success": true, "data": { "...": "..." }, "meta": { "requestId": "req_8a2Bf3kP", "environment": "sandbox" } } ``` ## Rate limits If you send too many requests, you get `429 RATE_LIMIT_EXCEEDED`. Responses don't include rate-limit headers, so retry with exponential backoff. ## Keep keys safe * Call the API from your server. Never ship a key in a browser or mobile app. * Load keys from environment variables or a secrets manager. Don't commit them. * Use a separate key per service, so you can revoke one without breaking the others. * To rotate, create a new key, deploy it, then delete the old one in the dashboard. A deleted key stops working. ## Next steps > **[Link UI](/guides/link-ui)** > > Let end users connect their devices. > **[Error codes](/guides/error-handling/error-codes)** > > Every authentication and access error. # Capabilities Source: https://docs.amps.ai/guides/capabilities Discover which commands, parameters, units, and timings a device accepts, and what happens when it can't do something. Every device read tells you exactly what that device accepts. Read it once, then build a push that will validate. ## Where capabilities live There's no separate endpoint. `commands`, `conflictStrategies`, and `settings` come back on the device read. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "conflictStrategies": ["cancel_and_replace", "queue_after"], "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 10, "max": 100 }, "power": { "unit": "kw", "min": 0, "max": 5, "step": 0.1 } }, "execution": ["immediate", "scheduled", "windowed"] }, "auto.balance": { "parameters": {}, "execution": ["immediate", "scheduled"] } } } } ``` The read mirrors the push. `commands.charge.parameters.target` on the read becomes `action.parameters.target` on the [push](/guides/push), sent as `{ "value": 80, "unit": "percent" }`. ## Present means supported If a key is present, the device supports it. If it's absent, the request is refused with `422 DEVICE_NOT_CAPABLE`. There is no `supported: false`. | You send | Not offered on the read | Result | | - | - | - | | A command | Key missing from `commands` | `422 DEVICE_NOT_CAPABLE` | | A parameter | Key missing from `parameters` | `422 DEVICE_NOT_CAPABLE` | | A different unit | `unit` doesn't match | `422 DEVICE_NOT_CAPABLE` | | A timing not in `execution` | | `422 DEVICE_NOT_CAPABLE` | | An `onConflict` not in `conflictStrategies` | | `422 DEVICE_NOT_CAPABLE` | | A value outside `min`/`max`, or off the `step` grid | | `422 PARAMETER_OUT_OF_RANGE` | `parameters: {}` means the command is supported and takes no parameters. ## When a device can't do something There are two different answers. Handle them differently. | Response | Meaning | What to do | | - | - | - | | `422 DEVICE_NOT_CAPABLE` | The device doesn't offer this. Amps also leaves out any command or setting it holds back on a device. | Don't retry. Ask for something the read declares. | | `503 CAPABILITY_PAUSED` | Amps has temporarily paused this capability. It stays on the read. | Keep it in your interface. Retry with backoff. | An immediate `DEVICE_NOT_CAPABLE` can carry `details` naming the refused value and what the device offers, such as `deviceCapabilities.supportedModes`, `supportedExecution`, or `supportedStrategies`. A failed action carries only `errorCode` and `errorMessage`. See [Error codes](/guides/error-handling/error-codes). ## Units and bounds Each parameter declares a `unit`, and optional `min`, `max`, and `step`. A missing bound is open-ended on that side. `step` is the increment a value must land on, counted from `min`. Bounds are per device, not per type. Two batteries from different manufacturers can declare different ranges for the same parameter, so always read them from the device. ## Execution `execution` lists the timings each command accepts. Details are in [Scheduling](/guides/scheduling). | Token | Push shape | | - | - | | `immediate` | No `start` or `end` | | `scheduled` | `start` only | | `windowed` | `end`, with or without `start` | Timings are per command. A battery can accept `charge` as windowed while `auto.balance` is immediate or scheduled only. `auto.*` commands are never windowed. ## Conflict strategies `conflictStrategies` lists the `onConflict` values the device accepts when a new push collides with an active action. See [Conflicts](/guides/push/conflicts). ## Commands by device type | Device type | Commands | Settings | | - | - | - | | Battery | `charge`, `discharge`, `idle`, `auto.balance`, `auto.reserve`, `auto.export` | Yes | | EV charger | `charge`, `idle`, `auto.charge_tariff`, `auto.charge_surplus_only`, `auto.charge_surplus_first` | Yes | | HVAC | `heat`, `cool`, `idle`, `auto.maintain`, `auto.schedule` | No | | Solar inverter | None | No | | Vehicle | None | No | This is the full set per type. Each device declares the subset it supports. ## Read-only device types Solar inverters and vehicles report state only. Their reads have no `commands`, `conflictStrategies`, or `settings`. A `POST` to `/solar-inverter/{deviceId}` or `/vehicle/{deviceId}` returns `405 METHOD_NOT_ALLOWED`, with the allowed methods in the `Allow` header. ## Live access and maturity Which brands your end users can link in live depends on your account and each integration's maturity: experimental, beta, or general. General integrations are open to accounts enabled for live; earlier stages need early-access approval. Sandbox includes every brand. None of this appears on the device read, which shows only what the linked device offers. **Coming soon.** Live control of thermostats. Sandbox supports every thermostat command. ## Next steps > **[Push](/guides/push)** > > Turn a capability into a command. > **[Settings](/guides/capabilities/settings)** > > Read and change persistent device configuration. > **[Scheduling](/guides/scheduling)** > > Run a command later or across a window. > **[Pull](/guides/pull)** > > Everything else on the device read. # Settings Source: https://docs.amps.ai/guides/capabilities/settings Read and change persistent device configuration, such as a battery's reserve or an EV charger's power ceiling. Settings are a device's standing configuration. Use them for limits the device should always respect, and [commands](/guides/push) for what it should do. ## Actions vs settings If it makes sense with a time window, it's an action. "Charge from 6pm to 10pm" is an action. "Never discharge below 20%" is a setting. | | Actions | Settings | | - | - | - | | Endpoint | `POST /battery/{deviceId}` | `POST /battery/{deviceId}/settings` | | Response | `202`, runs asynchronously | `200`, synchronous | | Scheduling | `start` / `end` | Not supported | | Recorded | As an action, with webhooks | No action record | Settings are available on batteries and EV chargers. The EV charger routes are `POST /ev-charger/{deviceId}` and `POST /ev-charger/{deviceId}/settings`. ## Read settings Settings come back on the device read, with the current value and the bounds this device accepts. ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "settings": { "safety_reserve": { "value": 10, "unit": "percent", "min": 10, "max": 100 }, "export_limit": { "value": 3000, "unit": "watts", "min": 0, "max": 30000 }, "scheduler_enabled": { "value": false } } } } ``` A setting appears when the device reports its value or limits. `value` is `null` when only limits are available, including when the manufacturer does not report the configured value. An unusable value is left out; independently reported limits remain available. In live, `settings` appears on readings taken from the device; see [Freshness](/guides/pull#freshness). ## Change settings Send only the settings you want to change. Numeric settings are `{ value, unit }`; on/off settings are `{ value }`. ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123/settings \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "discharge_floor": { "value": 15, "unit": "percent" }, "charge_ceiling": { "value": 95, "unit": "percent" } }' ``` ```json theme={null} { "success": true, "data": { "deviceId": "device_abc123", "updated": ["discharge_floor", "charge_ceiling"] } } ``` Every setting in the request is validated before anything is written. If one fails, none are applied. In sandbox, the next read shows the new values. ## Battery settings | Setting | Unit | Meaning | | - | - | - | | `safety_reserve` | `percent` | Lowest charge the battery will ever reach, even in a power cut. | | `discharge_floor` | `percent` | Lowest charge during normal operation. | | `charge_ceiling` | `percent` | Highest charge the battery will charge to. | | `export_limit` | `watts` | Maximum power sent to the grid. | | `max_charge_rate` | `amps` | Maximum charge current. | | `max_discharge_rate` | `amps` | Maximum discharge current. | | `scheduler_enabled` | | Whether the device's own scheduler is active. Read-only. | ## EV charger settings | Setting | Unit | Meaning | | - | - | - | | `max_charge_rate` | `kw` | Maximum charging power the charger will draw. | | `max_charge_current` | `amps` | The same ceiling, as current. | | `cable_lock` | | Whether the cable stays locked to the charger. | | `smart_charging` | | Whether the charger's own smart-charging schedule is active. When off, the charger doesn't start charges on its own. | `max_charge_rate` and `max_charge_current` set one ceiling in two units. A charger that holds a single rate refuses both in one request with `UNSUPPORTED_SETTING_COMBINATION`. Send one. The battery and EV charger `max_charge_rate` use different units: current in amps for batteries, power in kW for chargers. Always send the unit the read declares. ## Bounds Read `min`, `max` and optional `step` before writing. Limits can narrow the setting's shared range for this device. `step` counts from `min`, or from 0 when `min` is absent. Out-of-range and off-step values are refused without rounding or clamping. ## Errors | Code | Status | Cause | | - | - | - | | `EMPTY_SETTINGS` | 400 | The body has no settings. | | `INVALID_REQUEST_BODY` | 400 | The body isn't a map of settings. | | `DEVICE_NOT_CAPABLE` | 422 | The device doesn't offer this setting, or it's read-only, such as `scheduler_enabled`. `details.availableSettings` or `details.setting` says which. | | `INVALID_SETTING_UNIT` | 422 | `unit` doesn't match the declared unit. | | `INVALID_SETTING_VALUE` | 422 | Wrong value type. | | `SETTING_OUT_OF_RANGE` | 422 | Outside the device's `min`/`max`, off its increment grid, or beyond a narrower limit the connected unit reports. | | `UNSUPPORTED_SETTING_COMBINATION` | 422 | Two settings in the request set the same thing. | | `CAPABILITY_PAUSED` | 503 | Amps has temporarily paused this setting. Retry with backoff. | | `SETTINGS_STORE_UNAVAILABLE` | 503 | Settings are temporarily unavailable. Retry. | Manufacturer errors can also come back directly, for example `SCHEDULER_ACTIVE` (409) on a battery or `VEHICLE_NOT_CONNECTED` (409) on a charger. See [Error codes](/guides/error-handling/error-codes). ## Next steps > **[Capabilities](/guides/capabilities)** > > How devices declare what they accept. > **[Push](/guides/push)** > > Send commands to a device. > **[Error codes](/guides/error-handling/error-codes)** > > Every code, its status, and what triggers it. # Error handling Source: https://docs.amps.ai/guides/error-handling How errors are shaped, where they appear, and when to retry. Every error has the same shape and a stable `code`. Branch on the code, not the message. ## Response shape ```json theme={null} { "success": false, "error": { "code": "PARAMETER_OUT_OF_RANGE", "message": "One or more parameters are outside the supported range.", "details": { "parameter": "target", "value": 5, "min": 10, "max": 100, "unit": "percent" } }, "meta": { "requestId": "req_8a2Bf3kP", "timestamp": "2026-06-01T10:30:00.000Z", "path": "/battery/device_abc123" } } ``` | Field | | | - | - | | `error.code` | Stable identifier. Branch on this. | | `error.message` | Fixed text for the code. Don't parse it. | | `error.details` | Context for fixing the request, such as allowed ranges or what the device offers. Only present when relevant, so check before reading. | | `meta.requestId` | Include this when contacting support. | Successful responses use the same wrapper, with `data` in place of `error`. ## Where errors appear Errors reach you in two places: * **The HTTP response.** Validation, authentication, and capability errors are returned immediately, before anything is sent to the device. * **The action.** Once a command is accepted (`202`), it can still fail at the device. The action's `state` becomes `failed` and `errorCode` holds the code. You'll see it on [`GET /actions/{id}`](/guides/push) and in the [`push.failed` webhook](/guides/webhooks/events). ```json theme={null} { "id": "action_qjSucnQFAk", "state": "failed", "errorCode": "DEVICE_OFFLINE", "errorMessage": "The device is currently offline at the manufacturer." } ``` A failed action carries `errorCode` and `errorMessage` only, never `details`. ## When to retry | Status | Meaning | What to do | | - | - | - | | `400`, `422` | The request is invalid for this device. | Fix the request using `details`. Don't retry unchanged. | | `401`, `403` | Key, access, or end-user account problem. | Check your key and environment, or ask the end user to reconnect. Don't retry. | | `404` | Device or action not found. | Don't retry. | | `409` | Conflicts with another action or the device's state. | See [Conflicts](/guides/push/conflicts). | | `410` | Device offline, or Amps didn't receive an action's result in time. | Retry `DEVICE_OFFLINE` once the device is back. For `STALE_ACTION`, read the device with `?expedite=true` before you send the command again. | | `429` | Rate limited. | Retry with exponential backoff. | | `502`, `503`, `504` | The manufacturer is unreachable or slow, or Amps has paused this capability. | Retry with exponential backoff. | | `500` | Unexpected error. | Retry once, then contact support with the `requestId`. | | `501` | Not available yet. | Don't retry. | `DEVICE_NOT_CAPABLE` never changes on retry: the device doesn't offer what you asked for. `CAPABILITY_PAUSED` is temporary, so keep the feature and retry later. `INVALID_OEM_RESPONSE` is a `502` that won't clear on retry. Amps doesn't retry failed commands for you. ## Next steps > **[Error codes](/guides/error-handling/error-codes)** > > Every code, its status, and what triggers it. > **[Conflicts](/guides/push/conflicts)** > > Handle `409` responses when actions collide. # Error codes Source: https://docs.amps.ai/guides/error-handling/error-codes Every error code the Amps API returns. See [Error handling](/guides/error-handling) for the response shape and retry rules. ## Authentication and access | Code | Status | Description | | - | - | - | | `UNAUTHORIZED` | 401 | No API key was sent. | | `INVALID_API_KEY` | 401 | The API key is unrecognised or revoked. | | `EXPIRED_TOKEN` | 401 | The API key has expired. | | `FORBIDDEN` | 403 | Access to this resource is forbidden. | | `INSUFFICIENT_PERMISSIONS` | 403 | The API key lacks the required permission. | | `LIVE_ACCESS_DISABLED` | 403 | Your account isn't enabled for live yet. | | `CONSENT_REVOKED` | 403 | The end user revoked access to this device. | | `DEVICE_OVERAGE` | 403 | Your account has exceeded its device limit. | | `RATE_LIMIT_EXCEEDED` | 429 | Your API key has exceeded its rate limit. | ## Request validation | Code | Status | Description | | - | - | - | | `VALIDATION_ERROR` | 400 | A query parameter is out of bounds, or the body isn't valid JSON. | | `INVALID_REQUEST_BODY` | 400 | The request body is invalid, for example an unknown key or a `start` with a UTC offset. | | `EMPTY_SETTINGS` | 400 | A settings update must include at least one setting. | | `PAYLOAD_TOO_LARGE` | 413 | The request body is too large. | | `UNSUPPORTED_MEDIA_TYPE` | 415 | Send `Content-Type: application/json`. | | `METHOD_NOT_ALLOWED` | 405 | The method isn't allowed on this route, such as a `POST` to a solar inverter. | ## Devices and actions | Code | Status | Description | | - | - | - | | `NOT_FOUND` | 404 | The resource doesn't exist. | | `DEVICE_TYPE_MISMATCH` | 404 | The device exists but isn't this device type. Check the route. | | `ACTION_NOT_FOUND` | 404 | The action doesn't exist. | | `ACTION_NOT_CANCELLABLE` | 409 | Only `scheduled` actions can be cancelled. | | `GONE` | 410 | The resource is no longer available. | ## Capabilities | Code | Status | Description | | - | - | - | | `DEVICE_NOT_CAPABLE` | 422 | The device doesn't offer what you asked for: a command, parameter, unit, timing, `onConflict` strategy, setting, or charging-session history. Don't retry. | | `CAPABILITY_PAUSED` | 503 | Amps has temporarily paused this capability. Retry with backoff. | On an immediate response, `DEVICE_NOT_CAPABLE` can carry `details`. Read each field by presence. | Refused | `details` can include | | - | - | | Command | `deviceCapabilities.supportedModes` | | Parameter | `unsupportedParameters`, `deviceCapabilities.supportedParameters` | | Unit | `parameter`, `providedUnit`, `supportedUnits` | | Timing | `requestedExecution`, `supportedExecution` | | `onConflict` strategy | `requestedStrategy`, `supportedStrategies` | | Unknown setting | `availableSettings` | | Read-only setting | `setting` | See [Capabilities](/guides/capabilities#when-a-device-cant-do-something). ## Commands | Code | Status | Description | | - | - | - | | `PARAMETER_OUT_OF_RANGE` | 422 | A parameter is outside the device's `min`/`max`, or off its `step` grid. | | `UNSUPPORTED_PARAMETER_COMBINATION` | 422 | Two parameters cap the same thing, such as `power` and `current`. Send one. | ## Scheduling | Code | Status | Description | | - | - | - | | `START_IN_PAST` | 422 | `start` is in the past. | | `START_OUT_OF_RANGE` | 422 | `start` is more than 30 days ahead. | | `START_INVALID_FORMAT` | 422 | `start` isn't a real date and time. | | `START_NONEXISTENT_WALL_CLOCK` | 422 | `start` falls in a daylight-saving gap. | | `INVALID_TIME_WINDOW` | 422 | `end` is in the past, more than 30 days ahead, not after `start`, or a window the device can't hold. See `details.reason`. | | `TIMEZONE_UNRESOLVED` | 422 | The device's timezone is unknown. | | `INVALID_TIMEZONE` | 422 | The device's timezone is invalid. | ## Conflicts | Code | Status | Description | | - | - | - | | `CONFLICT` | 409 | Another action is active on this device. | | `CONFLICT_IN_EXECUTION` | 409 | A conflicting action is still on its way to the device, or its window is ending right now. Retry once it finishes. | | `SCHEDULER_ACTIVE` | 409 | A schedule on the device must be cleared first. | | `SCHEDULER_FULL` | 409 | The device's schedule is full. | | `MODE_OVERRIDDEN` | 409 | The device is in a state that overrides this command. | | `VPP_LOCKED` | 409 | Another control programme has locked the device. | | `VEHICLE_NOT_CONNECTED` | 409 | No vehicle is plugged in to the charger. | ## Settings | Code | Status | Description | | - | - | - | | `INVALID_SETTING_UNIT` | 422 | The unit is invalid. | | `INVALID_SETTING_VALUE` | 422 | The value is the wrong type. | | `SETTING_OUT_OF_RANGE` | 422 | The value is outside the allowed range, or beyond a narrower limit the connected unit reports. | | `UNSUPPORTED_SETTING_COMBINATION` | 422 | Two settings in the request set the same thing. | | `SETTINGS_STORE_UNAVAILABLE` | 503 | Settings are temporarily unavailable. Retry. | ## End-user account These come from the manufacturer account the end user linked. Most are fixed by reconnecting through [Link UI](/guides/link-ui). | Code | Status | Description | | - | - | - | | `INVALID_CREDENTIALS` | 401 | The manufacturer rejected the end user's credentials. | | `MFA_REQUIRED` | 403 | The manufacturer requires multi-factor authentication. | | `INVALID_MFA_CODE` | 403 | The multi-factor code wasn't accepted. | | `ACCOUNT_LOCKED` | 403 | The manufacturer account is locked. The end user must unlock it with the manufacturer. | | `DEVICE_UNAUTHORIZED` | 403 | The end user's account can no longer control this device. | | `UNSUPPORTED_CREDENTIAL_TYPE` | 400 | The stored credential type isn't supported for this device. | | `UNSUPPORTED_AUTH_PATH` | 400 | The authentication method isn't supported. | | `BIND_NOT_SUPPORTED` | 400 | The manufacturer doesn't support linking this device. | | `CREDENTIAL_NOT_FOUND` | 404 | No stored credential for this device. | | `NO_DEVICES_FOUND` | 404 | The linked account has no devices. | ## Device and manufacturer These often appear on a failed action's `errorCode` rather than as an HTTP response. | Code | Status | Description | | - | - | - | | `DEVICE_OFFLINE` | 410 | The device is offline. Retry once it's back. | | `DEVICE_NOT_FOUND` | 404 | The manufacturer can't find the device. | | `COMMAND_FAILED` | 400 | The manufacturer couldn't complete the command. | | `COMMAND_NOT_APPLIED` | 409 | Amps read the device back and the command hadn't taken effect. | | `INVALID_PARAMETERS` | 400 | One or more parameters were invalid. | | `INVALID_OEM_PARAMETERS` | 400 | The manufacturer rejected one or more parameters. | | `INVALID_OEM_RESPONSE` | 502 | The manufacturer returned data Amps couldn't use. `details.fields` names each reading. Don't retry. | | `STALE_ACTION` | 410 | Amps didn't receive the action's result in time. The device may have carried out the command, so read the device with `?expedite=true` and check `state` before you send it again. A windowed command still [ends at `end`](/guides/scheduling#when-a-window-ends). | | `RATE_LIMITED` | 429 | The manufacturer is rate limiting. Retry with backoff. | | `NETWORK_ERROR` | 502 | The manufacturer couldn't be reached. Retry with backoff. | | `SERVICE_UNAVAILABLE` | 503 | The manufacturer is unavailable. Retry with backoff. | | `TIMEOUT` | 504 | The manufacturer didn't respond in time. Retry with backoff. | | `SIMULATED_FAILURE` | 503 | A simulated failure in sandbox. | ## Server | Code | Status | Description | | - | - | - | | `INTERNAL_ERROR` | 500 | Unexpected error. Retry once, then contact support. | | `UNKNOWN_ERROR` | 500 | Unclassified manufacturer response. Retry once, then contact support. | | `DEFERRED_SCHEDULE_FAILED` | 500 | A scheduled action couldn't be set up. Resubmit it. | | `UNROUTABLE_ACTION_TYPE` | 500 | The command never reached the device. Contact support with the `requestId`. | | `NOT_IMPLEMENTED` | 501 | This endpoint isn't available yet. | | `BAD_GATEWAY` | 502 | An upstream service returned an invalid response. | | `GATEWAY_TIMEOUT` | 504 | An upstream service didn't respond in time. | # Link UI Source: https://docs.amps.ai/guides/link-ui Send end users to a hosted page to connect their devices, then find those devices through the API. Link UI is a hosted page where your end users sign in to their device manufacturer and choose which devices to share with you. You send them a URL; they come back to your app with their devices linked. ## Build the URL Each Link UI app has an `appId`. Create one in the [dashboard](https://app.amps.ai) under **Link UI**, which also shows the app's URL. ``` https://auth.amps.ai/{appId}?externalUserRef=user_8841 ``` | Parameter | | | - | - | | `externalUserRef` | Your ID for this end user. Up to 100 characters: letters, digits, and `.` `_` `+` `-` `@`. Other characters make Link UI generate a random ID instead. | | `sandbox` | `true` to link sandbox devices. Omit for live. | Live links only work once your account is enabled for live. An out-of-range value in any parameter makes Link UI ignore all of them, including `sandbox`, so validate them before you build the URL. [Customization](/guides/link-ui/customization) covers the parameters that shape the flow. ## What the end user does **1. Pick a device type** ![Link UI device type selection](https://mintcdn.com/ampsai/0nXXb16E_PzfRg-N/images/link-ui-step-1.png?fit=max\&auto=format\&n=0nXXb16E_PzfRg-N\&q=85\&s=5db6a90eeffcef8eb82dde65e12e6745) **2. Pick a manufacturer** ![Link UI manufacturer selection](https://mintcdn.com/ampsai/0nXXb16E_PzfRg-N/images/link-ui-step-2.png?fit=max\&auto=format\&n=0nXXb16E_PzfRg-N\&q=85\&s=fe7d379b340a70b0036dd7c177ca5299) **3. Agree to share their devices** **4. Sign in to the manufacturer** On Link UI, on the manufacturer's own page, or by entering details printed on the device. Completes MFA if asked. Link UI fills in the device's time zone from the browser, and the user can correct it. ![Link UI manufacturer sign-in](https://mintcdn.com/ampsai/0nXXb16E_PzfRg-N/images/link-ui-step-3.png?fit=max\&auto=format\&n=0nXXb16E_PzfRg-N\&q=85\&s=e207e704c4f73b279640e56dd1ddd3ee) **5. Select the devices to link** **6. Return to your app** Clicking **Return to** sends them to your app's redirect URL. The redirect carries no parameters. Listen for the `device.connected` [webhook](/guides/webhooks/events), or list the user's devices. ## Find a user's devices The `externalUserRef` you passed is the user's `userId` in the API. Filter any device list by it. ```bash theme={null} curl "https://api.amps.ai/battery?userId=user_8841" \ -H "x-api-key: $AMPS_API_KEY" ``` If you omit `externalUserRef`, Link UI generates a random ID that you have no way to read back. Always pass your own. ## Sandbox Create the app with the dashboard switched to sandbox. Its URL includes `sandbox=true`. For manufacturers that use a username and password or an API key, any value is accepted and the account has two devices. Manufacturers that sign in on their own page approve automatically. To test failures, enter a value starting with `error`, or the MFA code `000000`. ``` https://auth.amps.ai/{appId}?sandbox=true&externalUserRef=test_user_1 ``` Devices linked this way are visible to your `sk_test_` key. ## Reconnect When a manufacturer stops accepting a user's credentials, you get a `device.disconnected` webhook with a `reconnectionUrl`. Add `externalUserRef` with the user's ID to that URL, then send the user there. They sign in again, their devices keep the same IDs, and you get `device.reconnected`. ``` {reconnectionUrl}&externalUserRef=user_8841 ``` Without it, Link UI can't match the devices to the user. After reconnecting, Link UI sends the user to your app's reconnect redirect URL. ## Revoke consent When a user disconnects in your app, revoke their consent. Omit `deviceIds` to revoke every device the user linked. ```bash theme={null} curl -X DELETE https://api.amps.ai/users/user_8841/consent \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "deviceIds": ["device_abc123"] }' ``` ```json theme={null} { "success": true, "data": { "userId": "user_8841", "revokedDeviceCount": 1, "revokedDeviceIds": ["device_abc123"] } } ``` When no other link uses the device, and the manufacturer supports it, Amps also hands the device back to its owner at the manufacturer. If that step fails, the request fails and nothing is revoked. Requests for a revoked device return `403 CONSENT_REVOKED`. A `userId` with no matching devices returns `404 NOT_FOUND`. The user can grant access again by linking the device through Link UI with the same `externalUserRef`. ## Next steps > **[Customization](/guides/link-ui/customization)** > > Your name, logo, manufacturers, and flow options. > **[Webhooks](/guides/webhooks)** > > Get notified when devices connect or disconnect. > **[Pull](/guides/pull)** > > Read the state of a linked device. # Link UI customization Source: https://docs.amps.ai/guides/link-ui/customization Set your app's name, logo, redirect URLs, and device types, and shape the flow with URL parameters. Configure how Link UI looks and which devices it offers in the dashboard. Shape the flow for a single visit with URL parameters. ## App settings Edit these in the [dashboard](https://app.amps.ai) under **Link UI**. Each app belongs to the environment the dashboard was switched to when you created it. | Setting | | | - | - | | Link UI name | Shown in the header, on the consent screen, and on the **Return to** button. Up to 30 characters. | | Logo | Square SVG, PNG, or JPG, between 48×48 and 500×500 px. | | Redirect URL | Where the user goes after linking devices. | | Reconnect redirect URL | Where the user goes after [reconnecting](/guides/link-ui#reconnect). | | Device types | The device types users can pick. Selecting a type offers every manufacturer available for it. | | Beta manufacturers | Live apps only. Opt in to manufacturers in beta. | ## URL parameters Add these to the Link UI URL alongside [`externalUserRef` and `sandbox`](/guides/link-ui#build-the-url). | Parameter | Values | Effect | | - | - | - | | `deviceType` | `battery`, `ev_charger`, `hvac`, `solar_inverter`, `vehicle` | Skips the device type step. | | `maxDevices` | `1` to `100` | Caps how many devices the user can select. `1` makes it a single choice. | | `skipDeviceSelection` | `true` | Selects every discovered device up front. The user still confirms. | ``` https://auth.amps.ai/{appId}?externalUserRef=user_8841&deviceType=battery&maxDevices=1 ``` ## Next steps > **[Link UI](/guides/link-ui)** > > URL, end-user identity, reconnect, and consent. > **[Webhooks](/guides/webhooks)** > > Get notified when devices connect. # Pull Source: https://docs.amps.ai/guides/pull Read a device's state, list devices, and understand how fresh the data is. Read what a device is doing now, what it can do, and what Amps last told it to do. The read shape is the same for every device type; only the `state` fields change. ## Read a device ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "vendor": "example_vendor_a", "sync": { "available": true, "lastPulledAt": "2026-06-01T10:14:23.000Z" }, "metadata": { "model": "Hybrid 5kWh", "source": "cache", "cacheType": "normal" }, "state": { "status": "charging", "level": 50, "capacity": 10.4, "chargeRate": 2.4, "dischargeLimit": 10, "currentMode": "charge" }, "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 0, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] } }, "conflictStrategies": ["cancel_and_replace", "queue_after"], "settings": { "discharge_floor": { "value": 10, "unit": "percent", "min": 0, "max": 100 } }, "lastAction": { "id": "action_FoRvdqounP", "command": "charge", "state": "completed", "links": { "self": "/actions/action_FoRvdqounP" } }, "currentSchedule": null } } ``` The same route exists for every type: `/battery`, `/ev-charger`, `/hvac`, `/solar-inverter`, `/vehicle`. | Field | | | - | - | | `state` | What the device is doing now. Fields per type are [listed below](#state-fields-by-device-type). | | `sync` | `available` is `false` when the reading is a stored fallback. `lastPulledAt` is when the reading was taken from the device. | | `metadata` | `model`, plus where the reading came from. See [Freshness](#freshness). | | `commands`, `conflictStrategies` | What the device accepts. See [Capabilities](/guides/capabilities). | | `settings` | Persistent configuration with current values. See [Settings](/guides/capabilities/settings). Batteries and EV chargers only. | | `sessions` | EV chargers only. `true` when the charger reports charging-session history at `/ev-charger/{deviceId}/sessions`. | | `lastAction` | Summary of the most recent action you sent to this device. `null` until the first one. | | `currentSchedule` | Always `null` for now. | Solar inverters and vehicles are read-only. Their reads carry `id`, `vendor`, `sync`, `metadata`, and `state`, and nothing else. ## What the device is doing vs what it was told `state.status` is what the battery is doing. `state.currentMode` describes operating intent and does not prove a command took effect. They can differ: a `charge` whose target is already met reads `status: "idle"`, `currentMode: "charge"`, `chargeRate: 0`. EV chargers report the same split with `isCharging` and `activeControlMode`. Compare `state` with `lastAction` to spot changes made outside Amps, such as the homeowner switching mode in the manufacturer's app. ## `lastAction` A summary for rendering a device card in one call. For parameters and the full result, follow `links.self` to [`GET /actions/{id}`](/guides/push). ```json theme={null} { "id": "action_FoRvdqounP", "command": "charge", "state": "failed", "createdAt": "2026-06-01T09:15:00.000Z", "updatedAt": "2026-06-01T09:15:32.000Z", "errorCode": "DEVICE_OFFLINE", "errorMessage": "The device is currently offline at the manufacturer.", "links": { "self": "/actions/action_FoRvdqounP" } } ``` `updatedAt` is the last state change. `errorCode` and `errorMessage` are `null` unless `state` is `failed`. A just-sent action can take a moment to appear here, so read it by id if you need its state straight after a push. ## Freshness Live reads are cached. `metadata.source` tells you where a reading came from. | `metadata.source` | Meaning | `sync.available` | | - | - | - | | `cache` | A recent reading. Up to 15 minutes old, or 1 minute with `?expedite=true`. `cacheType` says which. | `true` | | `live` | Read from the device during this request. | `true` | | `fallback` | The device couldn't be reached, so this is the last stored reading. Check `sync.lastPulledAt`. | `false` | | `projection` | Simulated sandbox state. See [Sandbox](#sandbox). | `true` | For fresher data, add `?expedite=true`. It returns a reading no more than a minute old, calling the manufacturer if needed. Use it sparingly. ```bash theme={null} curl "https://api.amps.ai/battery/device_abc123?expedite=true" \ -H "x-api-key: $AMPS_API_KEY" ``` A completed push doesn't refresh the cache. To react to a command, use the [`push.completed` webhook](/guides/webhooks/events), then read with `?expedite=true` if you need the new state. In live, `settings` is included when the reading comes from the device (`source: "live"`) and omitted on cached and fallback readings. A setting the device reports in an unusable form, or outside its declared range, is left out rather than shown. When Amps has temporarily paused reads for a manufacturer, you get the last stored reading with `metadata.degraded: true`, or `503 CAPABILITY_PAUSED`. Retry with backoff. If the device is offline or unknown at the manufacturer, the read returns an error (`DEVICE_OFFLINE`, `DEVICE_NOT_FOUND`) instead of stale data. See [Error codes](/guides/error-handling/error-codes). ## List devices ```bash theme={null} curl "https://api.amps.ai/battery?userId=user_abc123&limit=20&offset=0" \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "items": [{ "id": "device_abc123", "state": { "status": "idle", "level": 67 }, "lastAction": null }], "pagination": { "limit": 20, "offset": 0, "total": 1, "hasMore": false } } } ``` | Query | | | - | - | | `userId` | Only devices linked by this end user. | | `limit` | 1 to 50. Default 10. | | `offset` | Default 0. | List entries carry the last stored reading, `commands`, `conflictStrategies`, and `lastAction`, but not `settings`. For current state, read the device by id. ## Sandbox Sandbox devices are simulated. Batteries, EV chargers, and thermostats report `source: "projection"`, and their state follows the commands you send: push `charge` and the next read shows `status: "charging"` with `level` rising toward the target. No cache, no waiting. * Values in motion change between reads. Compare against the command you sent, not an exact earlier number. * A windowed command is active between `start` and `end`, then the device goes idle and holds where the window left it. * With no commands, devices follow a daily pattern on UTC time, for example batteries charge around midday UTC. Solar inverters and vehicles follow the UTC daily pattern only, since they take no commands. Their reads report `cache` or `live`. ## State fields by device type ### Battery | Field | Unit | Meaning | | - | - | - | | `status` | | `charging`, `discharging`, `idle`, or `standby`. | | `level` | percent | State of charge, 0 to 100. | | `capacity` | kWh | Total capacity. | | `chargeRate` | kW | Current charge rate. Negative when discharging. | | `dischargeLimit` | percent | Reserve level. Absent when the manufacturer does not report it. | | `currentMode` | | Operating intent, such as `charge` or `auto.balance`. `unknown` means the intent could not be determined. | ### EV charger | Field | Unit | Meaning | | - | - | - | | `status` | | `available`, `charging`, `discharging`, `scheduled`, `error`, or `offline`. | | `isConnected` | | A vehicle is plugged in. | | `isCharging` | | The charger is delivering power to the vehicle now. | | `currentPower` | kW | Current power. Negative when exporting from the vehicle. | | `maxCurrent` | A | Maximum current the charger can deliver. | | `powerRateLimit` | kW | Configured maximum charging power. | | `sessionEnergy` | kWh | Energy delivered in the current session. | | `phases` | | Supply phases in use, 1 to 3. | | `voltage` | V | Supply voltage. | | `notChargingReason` | | Why a plugged-in vehicle isn't charging: `vehicle`, `target_reached`, `charger`, `load_management`, `authorization`, `schedule`, or `unknown`. | | `activeControlMode` | | The command the charger is running, such as `charge` or `auto.charge_tariff`. | Only `status`, `isConnected`, and `isCharging` are always present. The rest are omitted when the charger doesn't report them, never sent as zero. ### HVAC | Field | Unit | Meaning | | - | - | - | | `temperature` | °C | Current temperature. | | `active` | | Heating or cooling right now. | | `heatSetpoint` | °C | Heats below this. | | `coolSetpoint` | °C | Cools above this. | | `mode` | | `heat`, `cool`, `auto`, or `off`. | | `holdType` | | `permanent` when a fixed setpoint overrides the device's own program, `schedule` when it's following it. | ### Solar inverter | Field | Unit | Meaning | | - | - | - | | `status` | | `producing`, `idle`, `night_mode`, `error`, or `offline`. | | `producing` | | Producing power now. | | `currentPower` | kW | Current AC output. | | `energyTotal` | kWh | Lifetime production. | ### Vehicle | Field | Unit | Meaning | | - | - | - | | `batteryLevel` | percent | State of charge, 0 to 100. | | `range` | miles | Estimated remaining range. | | `plugged` | | Plugged in to a charger. | | `charging` | | Drawing charge now. | | `fullyCharged` | | Reached `chargeLimit`. | | `batteryCapacity` | kWh | Usable capacity. | | `chargeLimit` | percent | Configured charge limit. | | `chargeRate` | kW | Current charge rate. Negative when discharging. | | `chargeTimeRemaining` | minutes | Time to reach `chargeLimit`. | | `maxCurrent` | A | Maximum current the vehicle accepts. | ## Next steps > **[Capabilities](/guides/capabilities)** > > Read `commands` to build a valid push. > **[Push](/guides/push)** > > Send commands and track actions. > **[Webhooks](/guides/webhooks)** > > React to completed actions instead of polling. > **[Settings](/guides/capabilities/settings)** > > Read and change persistent device configuration. # Push Source: https://docs.amps.ai/guides/push Send a command to a device, choose when it runs, and track the action to completion. A push sends one command to one device. Every writable device type takes the same body, and every push creates an action you can track. ## Send a command Post to the device's route: `POST /battery/{deviceId}`, `POST /ev-charger/{deviceId}`, or `POST /hvac/{deviceId}`. ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "target": { "value": 80, "unit": "percent" }, "power": { "value": 3, "unit": "kw" } } } }' ``` Solar inverters and vehicles are read-only. They have no push route, so a `POST` to them returns `405 METHOD_NOT_ALLOWED`. ## Request body | Field | Required | | | - | - | - | | `action.command` | Yes | The command to run, such as `charge`, `idle`, or `heat`. | | `action.parameters` | No | Inputs the command accepts. Each is a Quantity. | | `action.start` | No | When to run. Omit to run now. | | `action.end` | No | When to stop and revert. | | `onConflict` | No | What to do if the device already has an active action. See [Conflicts](/guides/push/conflicts). | | `sandbox` | No | Sandbox only. Simulate a failure, for example `{ "result": "failed", "errorCode": "DEVICE_OFFLINE" }`. Ignored in live. | The commands, parameters, and bounds a device accepts are listed in its `commands` map. See [Capabilities](/guides/capabilities). ## Commands by device type | Device type | Commands | | - | - | | Battery | `charge`, `discharge`, `idle`, `auto.balance`, `auto.reserve`, `auto.export` | | EV charger | `charge`, `idle`, `auto.charge_tariff`, `auto.charge_surplus_only`, `auto.charge_surplus_first` | | HVAC | `heat`, `cool`, `idle`, `auto.maintain`, `auto.schedule` | A given device may support a subset. A command, parameter, or unit the device doesn't list returns `422 DEVICE_NOT_CAPABLE`, with the offending value and what the device supports in `details`. ## Quantities Every numeric parameter is a Quantity: a `value` and a `unit`. ```json theme={null} { "value": 80, "unit": "percent" } ``` Valid units are `percent`, `kw`, `watts`, `kwh`, `amps`, `volts`, `hours`, `minutes`, `celsius`, and `fahrenheit`. Each parameter accepts the unit declared in the device's `commands` map. A value outside the declared bounds returns `422 PARAMETER_OUT_OF_RANGE`. ## When it runs | Execution | Body | Behaviour | | - | - | - | | `immediate` | No `start`, no `end` | Runs now. | | `scheduled` | `start` only | Runs at `start`. | | `windowed` | `start` and `end` | Runs at `start`, stops at `end`. | | `windowed` | `end` only | Runs now, stops at `end`. | Each command lists the execution types it supports in its `execution` array. An unsupported one returns `422 DEVICE_NOT_CAPABLE` with `requestedExecution` and `supportedExecution` in `details`. Time format, time zones, and limits are covered in [Scheduling](/guides/scheduling). ## The response A push returns `202 Accepted` with the new action. Track it at `links.self`. ```json theme={null} { "success": true, "data": { "id": "action_qjSucnQFAk", "deviceId": "device_abc123", "deviceType": "battery", "command": "charge", "state": "acknowledged", "links": { "self": "/actions/action_qjSucnQFAk" } } } ``` Pushes that run now return `acknowledged`. Pushes with a `start` return `scheduled`, with `start` and `end` echoed as UTC timestamps. An immediate push may also carry `warnings`, such as no vehicle plugged in. The command is still accepted. If the manufacturer refuses an immediate command straight away, the push returns that error instead of `202`. The action is still recorded as `failed`, and a [`push.failed`](/guides/webhooks/events#push-failed) is sent. ## Action states | State | Meaning | | - | - | | `scheduled` | Waiting for its `start` time. Can be [cancelled](/guides/push/cancelling). | | `acknowledged` | Amps accepted the action for dispatch. Waiting for the result. | | `completed` | Successful dispatch, based on the command's acknowledgement. | | `failed` | The command didn't succeed, or Amps didn't receive its result in time. `errorCode` says why. | | `cancelled` | Cancelled before it ran. | States move `scheduled` → `acknowledged` → `completed` or `failed`. Immediate pushes start at `acknowledged`. `completed`, `failed`, and `cancelled` are final. A windowed action is `completed` while its window is still running. Its `endedAt` is set when the window closes. The acknowledgement can confirm receipt by the manufacturer's service without confirming device execution. Use [fresh device readings](/guides/pull#freshness) to check the effect separately. ## Check an action ```bash theme={null} curl https://api.amps.ai/actions/action_qjSucnQFAk \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "action_qjSucnQFAk", "state": "failed", "errorCode": "DEVICE_OFFLINE", "errorMessage": "The device is currently offline at the manufacturer.", "completedAt": "2026-05-07T10:30:05.000Z" } } ``` In production, use [webhooks](/guides/webhooks) instead of polling. ## List actions `GET /actions` returns actions across all your devices, newest first. ```bash theme={null} curl "https://api.amps.ai/actions?deviceId=device_abc123&state=scheduled" \ -H "x-api-key: $AMPS_API_KEY" ``` | Query parameter | | | - | - | | `deviceId` | Only this device. | | `state` | `scheduled`, `acknowledged`, `completed`, `failed`, or `cancelled`. | | `type` | Device type: `battery`, `ev_charger`, `hvac`, `solar_inverter`, or `vehicle`. | | `userId` | Only devices linked to this end user. | | `limit` | 1 to 50. Default 10. | | `offset` | Default 0. | The response is `data: { items, pagination }`, with the same item shape as `GET /actions/{actionId}`. ## Next steps > **[Conflicts](/guides/push/conflicts)** > > What happens when a device already has an active action. > **[Scheduling](/guides/scheduling)** > > Run commands later or inside a time window. > **[Webhooks](/guides/webhooks)** > > Get notified when an action completes or fails. > **[Capabilities](/guides/capabilities)** > > Find out what a device accepts before you push. # Cancelling Source: https://docs.amps.ai/guides/push/cancelling Cancel a scheduled action before it runs. Cancel an action that hasn't started yet. Only actions in `scheduled` state can be cancelled, on every type of device. ## Cancel by ID ```bash theme={null} curl -X POST https://api.amps.ai/actions/action_qjSucnQFAk/cancel \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "action_qjSucnQFAk", "state": "cancelled", "completedAt": "2026-05-08T11:32:00.000Z" } } ``` The API returns `202` with the cancelled action. It won't run, and no webhook is sent for it. If it was windowed, nothing happens at its `end`. ## What can be cancelled | State | Cancellable | | - | - | | `scheduled` | Yes | | `acknowledged` | No. It's already been sent to the device. | | `completed`, `failed`, `cancelled` | No. These are final. | Anything other than `scheduled` returns `409 ACTION_NOT_CANCELLABLE`. ```json theme={null} { "success": false, "error": { "code": "ACTION_NOT_CANCELLABLE", "message": "The action cannot be cancelled in its current state.", "details": { "description": "Action in state 'acknowledged' cannot be cancelled" } } } ``` To end a command that has already run, or a window that is still running, push a new one, such as `idle`, with `onConflict: "cancel_and_replace"`. ## Cancel and replace in one request If you're cancelling to send something else, push the new action with `onConflict: "cancel_and_replace"`. This cancels the scheduled action and accepts the new one in one request, so the old one can't start in between. See [Conflicts](/guides/push/conflicts). ## Find actions to cancel ```bash theme={null} curl "https://api.amps.ai/actions?deviceId=device_abc123&state=scheduled" \ -H "x-api-key: $AMPS_API_KEY" ``` ## Next steps > **[Conflicts](/guides/push/conflicts)** > > Replace or queue behind an active action. > **[Push](/guides/push)** > > Action states and how to track them. # Conflicts Source: https://docs.amps.ai/guides/push/conflicts What happens when you push to a device that already has an active action, and how to resolve it. A device runs one action at a time. Use `onConflict` to decide what happens when you push while another action still holds the device. ## What counts as a conflict An action on the same device that is `scheduled` or `acknowledged`, or a windowed action whose window is still running, even if it's `completed` or `failed`. Time windows aren't compared, so two scheduled actions with separate windows still conflict. `cancelled` actions, and finished actions with no window still running, never conflict. ## Strategies | `onConflict` | Behaviour | | - | - | | Omitted | Returns `409 CONFLICT`. Nothing changes. | | `cancel_and_replace` | Cancels a scheduled action, or takes over a window that is still running, and accepts yours. | | `queue_after` | Accepts yours and starts it when the existing action's `end` is reached. | ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "discharge", "parameters": { "target": { "value": 30, "unit": "percent" } } }, "onConflict": "cancel_and_replace" }' ``` A replaced `scheduled` action moves to `cancelled`; a running window keeps its state and original result. If Amps cannot confirm that the running command can be replaced, your push is refused. ## Supported strategies Each device lists the strategies it supports in `conflictStrategies` on its read response. ```json theme={null} { "success": true, "data": { "id": "device_abc123", "conflictStrategies": ["cancel_and_replace", "queue_after"] } } ``` Sending a strategy the device doesn't list returns `422 DEVICE_NOT_CAPABLE` with `requestedStrategy` and `supportedStrategies` in `details`, even when there's no conflict. Many devices list `cancel_and_replace` only. ## The 409 response ```json theme={null} { "success": false, "error": { "code": "CONFLICT", "message": "The request conflicts with the current resource state.", "details": { "reason": "no_strategy_supplied", "conflictingActionIds": ["action_BqZ9oVoc6f"], "strategies": ["cancel_and_replace", "queue_after"] } } } ``` `details.strategies` lists what will resolve it. Resend with one of them. `conflictingActionIds` is always an array. | `code` | `details.reason` | Cause | What to do | | - | - | - | - | | `CONFLICT` | `no_strategy_supplied` | You didn't send `onConflict`. | Resend with a strategy from `details.strategies`. | | `CONFLICT` | `conflicting_action_not_windowed` | `queue_after`, but the existing action has no `end`. | Use `cancel_and_replace`. | | `CONFLICT_IN_EXECUTION` | `conflicting_action_in_progress` | The existing action is `acknowledged` and still on its way to the device, or Amps is ending its window right now. | Wait for it to finish, then resend. | ## How queue\_after works The existing action must have an `end`. Your action is created as `scheduled` and starts at that `end`. If your `start` is already later, it's kept. A windowed push can't be moved, because its times are fixed. If the existing action ends after your window opens, you get `422 INVALID_TIME_WINDOW` with `details.reason: "queue_after_overlaps_window"`. Use `cancel_and_replace` or pick a later window. You also can't queue behind an `auto.*` command, because its `end` isn't a time the device is busy until. That returns `422 DEVICE_NOT_CAPABLE`. ## Conflicts on the device Some conflicts come from the device rather than from Amps. These are also `409`s. | Code | Cause | | - | - | | `SCHEDULER_ACTIVE` | The device has its own schedule, set outside Amps, that blocks the command. | | `SCHEDULER_FULL` | The device's schedule has no free slots. | | `MODE_OVERRIDDEN` | The device is in a state that overrides the command. | | `VPP_LOCKED` | Another control programme has locked the device. | `SCHEDULER_ACTIVE` can be returned when you submit a windowed push that overlaps the device's own schedule. It can also arrive later as the `errorCode` on a failed action, because the device's schedule can change before your action runs. The other three usually arrive on the failed action. ```json theme={null} { "success": false, "error": { "code": "SCHEDULER_ACTIVE", "message": "A schedule is currently active on the device and must be cleared first.", "details": { "reason": "foreign_scheduler_window_overlap", "recoveryStrategies": ["cancel_and_replace", "manual_clear_via_device_app"] } } } ``` If `recoveryStrategies` includes `cancel_and_replace`, resend with it to overwrite the overlapping entries on the device. Otherwise, ask the end user to clear the schedule in the manufacturer's app. ## Next steps > **[Cancelling](/guides/push/cancelling)** > > Cancel a scheduled action by ID. > **[Scheduling](/guides/scheduling)** > > Set `start` and `end` on a push. > **[Error codes](/guides/error-handling/error-codes)** > > Every code and its status. # Scheduling Source: https://docs.amps.ai/guides/scheduling Run a command later, or across a time window, in the device's local time. Add `start` to run a command later, or `end` to run it across a window. Times are the device's local wall-clock time, so you never deal with UTC offsets. ## Timing | Shape | Body | Action starts as | Behaviour | | - | - | - | - | | Immediate | No `start` or `end` | `acknowledged` | Runs now. | | Scheduled | `start` | `scheduled` | Runs at `start`. | | Windowed | `start` and `end` | `scheduled` | Runs from `start` until `end`. | | Windowed from now | `end` only | `acknowledged` | Runs now until `end`. | Each command lists the shapes it accepts in its `execution` array. Both windowed shapes need `windowed`. See [Capabilities](/guides/capabilities#execution). ## Schedule a window ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "target": { "value": 90, "unit": "percent" } }, "start": "2026-06-10T00:30:00", "end": "2026-06-10T04:30:00" } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_wd5ZqQtyBs", "command": "charge", "state": "scheduled" } } ``` The battery charges from 00:30 to 04:30 where it's installed, wherever your servers are. ## Time format Send `start` and `end` as local wall-clock time with no offset: `YYYY-MM-DDTHH:MM:SS`. Amps converts them using the device's timezone. | Value | Accepted | | - | - | | `2026-06-10T22:00:00` | Yes | | `2026-06-10T22:00:00Z` | No, `400 INVALID_REQUEST_BODY` | | `2026-06-10T22:00:00+01:00` | No, `400 INVALID_REQUEST_BODY` | | `30m`, `1.5h` | `start` only, and only without `end` | Relative durations run that long after Amps receives the request. A window needs absolute times for both `start` and `end`. ## Times come back in UTC Actions return `start` and `end` as UTC instants. A UK device in summer (BST, UTC+1): ```json theme={null} { "id": "action_wd5ZqQtyBs", "state": "scheduled", "start": "2026-06-09T23:30:00.000Z", "end": "2026-06-10T03:30:00.000Z" } ``` ## Limits | Rule | Error | | - | - | | `start` must be in the future. | `START_IN_PAST` | | `start` must be within 30 days. | `START_OUT_OF_RANGE` | | `start` must be a real date and time. | `START_INVALID_FORMAT` | | `end` must be in the future, within 30 days, and after `start`. | `INVALID_TIME_WINDOW` | | The device needs a known timezone for wall-clock times. | `TIMEZONE_UNRESOLVED`, `INVALID_TIMEZONE` | All return `422`. `INVALID_TIME_WINDOW` carries `details.reason`. A relative `start` doesn't need the device's timezone. ## Daylight saving | Case | Example (UK) | Behaviour | | - | - | - | | Time doesn't exist (clocks go forward) | `2026-03-29T01:30:00` | Rejected with `START_NONEXISTENT_WALL_CLOCK`. | | Time happens twice (clocks go back) | `2026-10-25T01:30:00` | Ambiguous. Pick a time outside the repeated hour. | `end` is compared with `start` in local time, so a window that crosses a clock change is valid as long as `end` is later on the wall clock. ## When a window ends Amps makes a best effort to end the command at `end`, leaving a replacement command alone. The action's `endedAt` records when Amps closed the window. ## Cancel a scheduled action An action can be cancelled while it's still `scheduled`. See [Cancelling](/guides/push/cancelling). Only one action can be active or scheduled per device. A new push that collides returns `409` unless you set `onConflict`. See [Conflicts](/guides/push/conflicts). ## Device schedules To have a device run the program it already holds, such as a thermostat's own schedule, send the `auto.schedule` command. **Coming soon.** Recurring schedules that Amps sets and runs on the device. ## Next steps > **[Push](/guides/push)** > > The action body and lifecycle. > **[Conflicts](/guides/push/conflicts)** > > Handle collisions with scheduled actions. > **[Webhooks](/guides/webhooks)** > > Get notified when a scheduled action runs. > **[Battery cookbook](/cookbooks/battery)** > > Charge in a cheap-rate window, and more. # Webhooks Source: https://docs.amps.ai/guides/webhooks Receive events when an action finishes or a device's connection changes. Amps sends an HTTP `POST` to your endpoint when an action reaches `completed` or `failed`, and when a device connects, reconnects, or disconnects. Use webhooks instead of polling `GET /actions/{actionId}`. ## Set up an endpoint 1. In the [dashboard](https://app.amps.ai), open **Settings → Webhooks**. 2. Pick the environment. Sandbox and live have separate endpoints. 3. Add your endpoint URL. 4. Copy the endpoint's signing secret (`whsec_...`) into your server's environment. Events from a sandbox key go to your sandbox endpoints. Events from a live key go to your live endpoints. You can add more than one endpoint per environment. ## Events | Event | Fires when | | - | - | | `push.completed` | An action reaches `completed`. | | `push.failed` | An action reaches `failed`. | | `device.connected` | An end user links a new device through Link UI. | | `device.reconnected` | An end user links a device that was already connected. | | `device.disconnected` | The device's manufacturer credentials stop working. | Payloads for each are in [Events](/guides/webhooks/events). No event is sent when an action is created, starts, or is cancelled. ## Payload The request body is the event's fields as flat JSON. There's no wrapper and no `type` field. ```json theme={null} { "actionId": "action_qjSucnQFAk", "deviceId": "device_abc123", "deviceType": "battery", "command": "charge", "parameters": { "target": { "value": 80, "unit": "percent" } }, "result": { "success": true }, "completedAt": "2026-05-07T10:30:05.000Z" } ``` Tell events apart by their fields. `errorCode` means `push.failed`. `actionId` without `errorCode` means `push.completed`. No `actionId` means a `device.*` event. See [Events](/guides/webhooks/events#identify-the-event). ## Handle a delivery 1. [Verify the signature](/guides/webhooks/verify-signatures) using the raw request body. 2. Skip it if you've already processed it. 3. Return a `2xx` quickly. Do slow work in the background. Any other response, or no response, is retried with exponential backoff. You can see each delivery attempt in the dashboard, and resend from there. ## Duplicates Deliveries are at least once, so the same event can arrive more than once. | Event | Dedupe on | | - | - | | `push.completed`, `push.failed` | `actionId`. Each action sends one final event. | | `device.*` | The `svix-id` header. It stays the same across retries. | ## Timing Events are sent after a short delay. In live, every event arrives about 10 seconds after it happens. In sandbox, the delay is longer so you have time to start a tunnel to your local machine: about 3 minutes for push events and `device.disconnected`, and about 1 minute for `device.connected` and `device.reconnected`. ## Test in sandbox Push to a sandbox device to get a `push.completed`. To get a `push.failed`, add a `sandbox` block to the push: ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge" }, "sandbox": { "result": "failed", "errorCode": "DEVICE_OFFLINE" } }' ``` An immediate push like this one returns the simulated error straight away, and the `push.failed` follows. Add a `start` to get a `202` first and the failure when the action runs. ## Next steps > **[Verify signatures](/guides/webhooks/verify-signatures)** > > Confirm each request came from Amps. > **[Events](/guides/webhooks/events)** > > Every event and its payload. > **[Push](/guides/push)** > > Action states and how actions finish. # Events Source: https://docs.amps.ai/guides/webhooks/events Every webhook event and its payload. Each event is delivered as a flat JSON body. See [Webhooks](/guides/webhooks) for setup, retries, and duplicates. ## Event list | Event | Fires when | | - | - | | `push.completed` | An action reaches `completed`. | | `push.failed` | An action reaches `failed`. | | `device.connected` | An end user links a new device through Link UI. | | `device.reconnected` | An end user links a device that was already connected. | | `device.disconnected` | The device's manufacturer credentials stop working. | **Coming soon.** Events for recurring schedules. ## Identify the event The body has no event type field. Branch on the fields present. ```ts theme={null} function eventType(event: Record): string { if ("actionId" in event) return "errorCode" in event ? "push.failed" : "push.completed"; if ("reconnectionUrl" in event) return "device.disconnected"; return "device.connected or device.reconnected"; } ``` `device.connected` and `device.reconnected` have the same fields. If you need to tell them apart, check whether you already know the `deviceId`. ## push.completed The action reached `completed`. See [action states](/guides/push#action-states) for what completion confirms and how to check the device effect. ```json theme={null} { "actionId": "action_qjSucnQFAk", "deviceId": "device_abc123", "deviceType": "battery", "command": "charge", "parameters": { "target": { "value": 80, "unit": "percent" } }, "result": { "success": true }, "completedAt": "2026-05-07T10:30:05.000Z" } ``` | Field | | | - | - | | `actionId` | The action. Same as `id` on `GET /actions/{id}`. | | `deviceId` | The device. | | `deviceType` | `battery`, `ev_charger`, or `hvac`. | | `command` | The command you sent. | | `parameters` | The parameters you sent, or `null`. | | `result` | `success` is `true`. A short `message` may be present. | | `completedAt` | When the action completed. | ## push.failed The action reached `failed`. Branch on `errorCode`. ```json theme={null} { "actionId": "action_qjSucnQFAk", "deviceId": "device_abc123", "deviceType": "battery", "command": "charge", "parameters": { "target": { "value": 80, "unit": "percent" } }, "result": { "success": false, "error": { "code": "DEVICE_OFFLINE", "message": "The device is currently offline at the manufacturer." } }, "errorCode": "DEVICE_OFFLINE", "errorMessage": "The device is currently offline at the manufacturer.", "failedAt": "2026-05-07T10:30:05.000Z" } ``` Same fields as `push.completed`, except: | Field | | | - | - | | `result` | The error, or `null`. Don't rely on it. Use `errorCode`. | | `errorCode` | Why it failed. See [Error codes](/guides/error-handling/error-codes). | | `errorMessage` | Human-readable. Don't parse it. | | `failedAt` | When the action failed. Replaces `completedAt`. | ## device.connected ```json theme={null} { "deviceId": "device_abc123", "deviceType": "battery", "timestamp": "2026-05-07T10:30:00.000Z" } ``` | Field | | | - | - | | `deviceId` | The device. | | `deviceType` | `battery`, `ev_charger`, `hvac`, `solar_inverter`, or `vehicle`. | | `timestamp` | When the event happened. | ## device.reconnected Same fields as `device.connected`. ```json theme={null} { "deviceId": "device_abc123", "deviceType": "battery", "timestamp": "2026-05-07T12:00:00.000Z" } ``` ## device.disconnected Sent when a read or command fails with `INVALID_CREDENTIALS`. A failed command also sends its own `push.failed`. ```json theme={null} { "deviceId": "device_abc123", "deviceType": "battery", "timestamp": "2026-05-07T11:00:00.000Z", "reconnectionUrl": "https://auth.amps.ai/app_abc123?manifestId=battery_example&reconnect=true" } ``` `reconnectionUrl` opens [Link UI](/guides/link-ui) so the end user can reconnect. It's usually present but can be missing, so handle both cases. ## Next steps > **[Verify signatures](/guides/webhooks/verify-signatures)** > > Confirm each request came from Amps. > **[Error codes](/guides/error-handling/error-codes)** > > Every `errorCode` a `push.failed` can carry. # Verify signatures Source: https://docs.amps.ai/guides/webhooks/verify-signatures Confirm a webhook came from Amps before you act on it. Every webhook is signed with your endpoint's signing secret. Verify the signature on every request, and reject any that fail. ## Headers Each request carries three headers. You need all three to verify. | Header | | | - | - | | `svix-id` | Unique ID for this message. Stays the same across retries. | | `svix-timestamp` | When the message was signed, in Unix seconds. | | `svix-signature` | One or more signatures, space-separated, each `v1,`. | The signing secret is on the endpoint in **Settings → Webhooks** in the [dashboard](https://app.amps.ai). It starts with `whsec_`. Sandbox and live endpoints have different secrets. ## Verify with the Svix library Amps signs webhooks with Svix, so you can use the `svix` package. ```bash theme={null} npm install svix ``` The signature covers the exact bytes Amps sent. Verify the raw body, not a parsed and re-serialised copy. **Express** ```ts theme={null} import express from "express"; import { Webhook } from "svix"; const app = express(); const wh = new Webhook(process.env.AMPS_WEBHOOK_SECRET!); // Use express.raw on this route so req.body is the unparsed Buffer. app.post("/webhooks/amps", express.raw({ type: "application/json" }), (req, res) => { let event: Record; try { event = wh.verify(req.body, { "svix-id": req.header("svix-id") ?? "", "svix-timestamp": req.header("svix-timestamp") ?? "", "svix-signature": req.header("svix-signature") ?? "", }) as Record; } catch { return res.status(400).send("Invalid signature"); } // event is the parsed, verified payload. handleEvent(event); res.sendStatus(204); }); ``` **Next.js** ```ts theme={null} // app/api/webhooks/amps/route.ts import { Webhook } from "svix"; const wh = new Webhook(process.env.AMPS_WEBHOOK_SECRET!); export async function POST(req: Request) { const body = await req.text(); let event: Record; try { event = wh.verify(body, { "svix-id": req.headers.get("svix-id") ?? "", "svix-timestamp": req.headers.get("svix-timestamp") ?? "", "svix-signature": req.headers.get("svix-signature") ?? "", }) as Record; } catch { return new Response("Invalid signature", { status: 400 }); } await handleEvent(event); return new Response(null, { status: 204 }); } ``` `verify` throws if the signature doesn't match or the timestamp is more than 5 minutes from now. On success it returns the parsed payload. ## Verify without the library If you can't use `svix`, compute the signature yourself: 1. Build the signed content: `{svix-id}.{svix-timestamp}.{raw body}`. 2. Base64-decode the part of your secret after `whsec_`. That's the key. 3. Compute HMAC-SHA256 of the signed content with the key, and base64-encode it. 4. Compare it, in constant time, with each `v1,` signature in `svix-signature`. Accept if any match. 5. Reject if `svix-timestamp` is more than 5 minutes from now. ```ts theme={null} import crypto from "node:crypto"; function verifyAmpsWebhook(rawBody: string, headers: Record, secret: string): boolean { const id = headers["svix-id"]; const timestamp = headers["svix-timestamp"]; const signatures = headers["svix-signature"]; if (!id || !timestamp || !signatures) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = crypto .createHmac("sha256", key) .update(`${id}.${timestamp}.${rawBody}`) .digest(); return signatures.split(" ").some((entry) => { const [version, sig] = entry.split(","); if (version !== "v1" || !sig) return false; const received = Buffer.from(sig, "base64"); return received.length === expected.length && crypto.timingSafeEqual(received, expected); }); } ``` ## Troubleshooting | Symptom | Likely cause | | - | - | | Every request fails | The body was parsed before verifying. Use the raw body. | | Every request fails | Wrong secret. Sandbox and live endpoints have different secrets. | | Some requests fail | Your server clock is off by more than 5 minutes. | ## Next steps > **[Webhooks](/guides/webhooks)** > > Endpoint setup, retries, and duplicates. > **[Events](/guides/webhooks/events)** > > Every event and its payload. # Introduction Source: https://docs.amps.ai/index One REST API to read and control batteries, EV chargers, HVAC, solar inverters, and vehicles across manufacturers. Amps is a REST API for home energy devices. You read state and send commands the same way whichever manufacturer built the device. > **[Quickstart](/quickstart)** > > Read a sandbox battery and send it a charge command. ## How it works 1. **Link.** Your end user connects their device account through [Link UI](/guides/link-ui), a hosted page. You never handle their manufacturer credentials. 2. **Read.** [Pull](/guides/pull) the device's state, for example `GET /battery/{deviceId}`. 3. **Control.** [Push](/guides/push) a command. The API returns `202` and runs it asynchronously. 4. **Confirm.** Get the result from a [webhook](/guides/webhooks) or `GET /actions/{id}`. ## Device types | Device type | Route | What you can do | | - | - | - | | Battery | `/battery` | Read, control, change settings | | EV charger | `/ev-charger` | Read, control, change settings, list charging sessions | | HVAC | `/hvac` | Read; control in sandbox | | Solar inverter | `/solar-inverter` | Read only | | Vehicle | `/vehicle` | Read only | What a device accepts depends on its manufacturer. Each device lists the commands and settings it accepts. See [Capabilities](/guides/capabilities). ## Sandbox and live Both use `https://api.amps.ai`. Your API key decides which one you reach: `sk_test_` for simulated sandbox devices, `sk_live_` for real ones. See [Authentication](/guides/authentication). ## Next steps > **[Quickstart](/quickstart)** > > Make your first read and command in sandbox. > **[Authentication](/guides/authentication)** > > Get an API key and pick an environment. > **[Link UI](/guides/link-ui)** > > Let end users connect their devices. > **[API Reference](/api-reference/introduction)** > > Every endpoint, request, and response. # Quickstart Source: https://docs.amps.ai/quickstart Read a sandbox battery, send it a charge command, and confirm the result. Read a battery's state, tell it to charge, and check that the command completed. You'll use a sandbox battery, so nothing real is controlled. > [!NOTE] > > You need a sandbox API key. [Request sandbox access](https://tally.so/r/D4zgZE). ## 1. Set your API key Sandbox keys start with `sk_test_`. Send yours in the `x-api-key` header on every request. ```bash theme={null} export AMPS_API_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` ## 2. Find a battery ```bash theme={null} curl https://api.amps.ai/battery \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "items": [{ "id": "device_abc123", "vendor": "example_vendor_a" }], "pagination": { "limit": 10, "offset": 0, "total": 1, "hasMore": false } } } ``` Copy the `id`. If the list is empty, connect a sandbox battery through [Link UI](/guides/link-ui). ## 3. Read its state ```bash theme={null} curl https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "device_abc123", "state": { "status": "idle", "level": 67, "capacity": 10.4, "currentMode": "auto.balance" }, "commands": { "charge": { "parameters": { "target": { "unit": "percent", "min": 0, "max": 100 } }, "execution": ["immediate", "scheduled", "windowed"] } } } } ``` `state` is what the battery is doing now. `commands` lists what it accepts: here, `charge` takes a `target` between 0 and 100 percent. ## 4. Send a charge command ```bash theme={null} curl -X POST https://api.amps.ai/battery/device_abc123 \ -H "x-api-key: $AMPS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": { "command": "charge", "parameters": { "target": { "value": 80, "unit": "percent" } } } }' ``` ```json theme={null} { "success": true, "data": { "id": "action_qjSucnQFAk", "command": "charge", "state": "acknowledged", "links": { "self": "/actions/action_qjSucnQFAk" } } } ``` The API returns `202 Accepted`. Commands run asynchronously, so this means Amps has accepted the command, not that the battery has started charging. ## 5. Check the result ```bash theme={null} curl https://api.amps.ai/actions/action_qjSucnQFAk \ -H "x-api-key: $AMPS_API_KEY" ``` ```json theme={null} { "success": true, "data": { "id": "action_qjSucnQFAk", "state": "completed", "result": { "success": true } } } ``` `state` moves from `acknowledged` to `completed` or `failed`. Read the battery again and it reports `status: "charging"`. In production, [subscribe to webhooks](/guides/webhooks) instead of polling. ## Next steps > **[Push](/guides/push)** > > Schedule commands, handle conflicts, cancel actions. > **[Pull](/guides/pull)** > > What device state contains and how fresh it is. > **[Webhooks](/guides/webhooks)** > > Get notified when an action completes. > **[Battery cookbook](/cookbooks/battery)** > > Charge in a cheap-rate window, auto-balance, and more.