> ## Documentation Index
> Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cap an EV Charger's Power

> Set an EV charger's maximum charging power as a device setting, then start and stop sessions with the charge and idle commands.

## Overview

An EV charger's maximum charging power is a ceiling the charger operates under, not a one-off command, so it is a device setting: `max_charge_rate` in kw, or `max_charge_current` in amps. Write it once and it persists across sessions until you change it or the charger reverts to its own defaults. Use it to balance against household consumption, throttle to a cheaper tariff window, or coordinate multiple chargers behind a single supply.

Commands can carry a rate cap too, and the difference is what makes each one the right tool. A `power` or `current` parameter on a `charge` command caps that session and nothing else. `max_charge_rate` is the standing ceiling the charger stays under whatever anyone asks it for next — including a session someone starts from the manufacturer's own app. Reach for the setting when the constraint belongs to the site, and for the command parameter when it belongs to the session. Commands run through `POST /ev-charger/{deviceId}`; the setting runs through `POST /ev-charger/{deviceId}/settings`. See [canonical actions](/concepts/canonical-actions) on the actions-versus-settings boundary.

**Pick one unit, not both.** Kilowatts and amperes describe the same ceiling, so a request carrying both leaves it ambiguous and is refused rather than resolved. On a command that is 422 `UNSUPPORTED_PARAMETER_COMBINATION` (`details.conflictingParameters`); on the settings write it is 422 `UNSUPPORTED_SETTING_COMBINATION` (`details.conflictingSettings`). A charger declaring both is normal and means you may write in either one.

<Callout icon="clock" color="#ED6D2C">
  **Coming soon.** Live control for EV chargers. The sandbox environment serves the full `commands` and `settings` surface, so the walkthrough below works against a sandbox device today. A live EV charger push returns 503 `NOT_YET_AVAILABLE` until the live path opens.
</Callout>

## Step 1: Read the current state

Every response is wrapped in `{ success, data, meta }`; the device sits under `data`.

```bash theme={null}
curl -X GET https://api.amps.ai/ev-charger/device_ev_001 \
  -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"
```

```json theme={null}
{
  "success": true,
  "data": {
    "id": "device_ev_001",
    "vendor": "example_vendor_a",
    "sync": { "available": true, "lastPulledAt": "2026-05-08T11:30:00.000Z" },
    "metadata": { "model": "7kW AC Charger", "source": "projection" },
    "state": {
      "status": "charging",
      "isConnected": true,
      "isCharging": true,
      "currentPower": 11,
      "maxCurrent": 32,
      "powerRateLimit": 11,
      "sessionEnergy": 12.6,
      "phases": 3,
      "voltage": 400
    },
    "conflictStrategies": ["cancel_and_replace", "queue_after"],
    "sessions": true,
    "commands": {
      "charge": {
        "parameters": {
          "power":   { "unit": "kw", "min": 1.4, "max": 22, "step": 0.1 },
          "current": { "unit": "amps", "min": 6, "max": 32, "step": 1 },
          "energy":  { "unit": "kwh", "min": 1, "max": 100 }
        },
        "execution": ["immediate", "scheduled", "windowed"]
      },
      "idle": { "parameters": {}, "execution": ["immediate", "scheduled"] },
      "auto.charge_tariff": { "parameters": {}, "execution": ["immediate", "scheduled"] }
    },
    "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 },
      "cable_lock":         { "value": true }
    },
    "vehicle": { "id": "device_veh456", "links": { "self": "/vehicle/device_veh456" } },
    "lastAction": null,
    "currentSchedule": null
  },
  "meta": {
    "requestId": "req_1uM5iVyL",
    "environment": "sandbox",
    "timestamp": "2026-05-08T11:30:00.000Z",
    "latencyMs": 33
  }
}
```

The charger is connected and drawing 11 kW. `data.settings.max_charge_rate` is the writable ceiling, bounded 0 to 50 kw in steps of 0.1 — a value between the increments is refused, so read `step` before you write. `commands` is abridged here; a charger that implements the full surface also advertises the two surplus modes, `auto.charge_surplus_only` (pauses when generation drops) and `auto.charge_surplus_first` (lets the grid top up). Every `auto.*` mode takes an empty `parameters` map and no `windowed` execution: the mode hands timing to the charger's own optimiser and runs until another command replaces it. `metadata.source` reports where the reading came from. Sandbox device reads always carry `source: "projection"` because sandbox devices are simulated, not physical hardware. In live, the value is one of `cache`, `live`, or `fallback`, naming the data-freshness tier the read came back from.

## Step 2: Cap the power

Write `max_charge_rate` through the settings endpoint. The body is a sparse map: send only the settings you want to change. The value is the canonical `{value, unit}` shape.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.amps.ai/ev-charger/device_ev_001/settings \
      -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "max_charge_rate": { "value": 7.4, "unit": "kw" }
      }'
    ```
  </Tab>

  <Tab title="Node">
    ```javascript theme={null}
    await fetch("https://api.amps.ai/ev-charger/device_ev_001/settings", {
      method: "POST",
      headers: {
        "x-api-key": process.env.AMPS_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        max_charge_rate: { value: 7.4, unit: "kw" },
      }),
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os, requests

    requests.post(
        "https://api.amps.ai/ev-charger/device_ev_001/settings",
        headers={
            "x-api-key": os.environ["AMPS_API_KEY"],
            "Content-Type": "application/json",
        },
        json={"max_charge_rate": {"value": 7.4, "unit": "kw"}},
    )
    ```
  </Tab>
</Tabs>

Settings are fire-and-forget. The response acknowledges the write and echoes the keys that changed, under the standard `data` envelope.

```json theme={null}
{
  "success": true,
  "data": {
    "deviceId": "device_ev_001",
    "updated": ["max_charge_rate"]
  },
  "meta": {
    "requestId": "req_2vN6jWzM",
    "environment": "sandbox",
    "timestamp": "2026-05-08T11:31:14.000Z",
    "latencyMs": 71
  }
}
```

A value outside 0 to 50, or one that misses the 0.1 kw grid, returns 422 `SETTING_OUT_OF_RANGE` with `min`, `max`, and `step` in `details` so you can round and retry without a second GET. An unknown key returns 422 `UNSUPPORTED_SETTING`; a read-only key returns 422 `READ_ONLY_SETTING`. Sending `max_charge_rate` and `max_charge_current` together returns 422 `UNSUPPORTED_SETTING_COMBINATION`. The bounds come from the device read, so check `data.settings` on the GET before you write.

## Step 3: Verify the cap applied

Read the device again. The new ceiling shows on `data.settings`, and live throughput settles to the cap.

```json theme={null}
{
  "success": true,
  "data": {
    "id": "device_ev_001",
    "vendor": "example_vendor_a",
    "sync": { "available": true, "lastPulledAt": "2026-05-08T11:32:02.000Z" },
    "metadata": { "model": "7kW AC Charger", "source": "projection" },
    "state": {
      "status": "charging",
      "isConnected": true,
      "isCharging": true,
      "currentPower": 7.4,
      "maxCurrent": 32,
      "powerRateLimit": 7.4,
      "sessionEnergy": 14.1,
      "phases": 3,
      "voltage": 400
    },
    "conflictStrategies": ["cancel_and_replace", "queue_after"],
    "sessions": true,
    "settings": {
      "max_charge_rate":    { "value": 7.4, "unit": "kw", "min": 0, "max": 50, "step": 0.1 },
      "max_charge_current": { "value": 32, "unit": "amps", "min": 6, "max": 32, "step": 1 },
      "cable_lock":         { "value": true }
    },
    "vehicle": { "id": "device_veh456", "links": { "self": "/vehicle/device_veh456" } },
    "lastAction": null,
    "currentSchedule": null
  },
  "meta": { "requestId": "req_3wO7kXaN", "environment": "sandbox", "timestamp": "2026-05-08T11:32:02.000Z", "latencyMs": 35 }
}
```

## Start and stop a session

The cap is configuration; starting and stopping a session is intent. Commands go in the canonical action envelope.

| Command                     | Use                                                                                       |
| --------------------------- | ----------------------------------------------------------------------------------------- |
| `charge`                    | Begin or resume a session on a connected vehicle.                                         |
| `idle`                      | Pause the session, holding it.                                                            |
| `auto.charge_tariff`        | Hand timing to the charger's own price optimiser. Runs until replaced.                    |
| `auto.charge_surplus_only`  | Charge from on-site generation the home is not using, pausing when that surplus runs out. |
| `auto.charge_surplus_first` | Use that surplus first and top up from the grid, so charging never pauses.                |

Read `commands` on the device to see which of these a given charger declares and what each one accepts — a command that is not in the map is refused with 422 `UNSUPPORTED_MODE`, which lists the ones that are.

Set the cap before starting a session to keep the first kWh inside a cheap rate, or change it mid-session as household demand spikes. The most recent setting wins.

```bash theme={null}
curl -X POST https://api.amps.ai/ev-charger/device_ev_001 \
  -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "action": { "command": "charge" } }'
```

```json theme={null}
{
  "success": true,
  "data": {
    "id": "act_ev_007",
    "deviceId": "device_ev_001",
    "deviceType": "ev_charger",
    "command": "charge",
    "parameters": null,
    "state": "acknowledged",
    "createdAt": "2026-05-08T12:00:01.000Z",
    "links": { "self": "/actions/act_ev_007" }
  },
  "meta": { "requestId": "req_4xP8lYbO", "environment": "sandbox", "timestamp": "2026-05-08T12:00:01.000Z", "latencyMs": 92 }
}
```

This is an immediate push, so the action begins life in `acknowledged` rather than passing through `scheduled`. To pause without unplugging, push `idle`. Only one command is in flight at a time; submit a second while the first is acknowledged and you get `409 CONFLICT`. Add `onConflict: "cancel_and_replace"` to drop the in-flight action and run the new one, or `queue_after` to hold yours until the running window closes.

`queue_after` needs an end to queue behind. An `auto.*` command has none, because it runs until another command replaces it, so queueing behind one returns 409 `CONFLICT` and points at `cancel_and_replace` instead. A conflicting action still mid-flight returns 409 `CONFLICT_IN_EXECUTION` with an empty strategy list, which means wait for it.

## Why this works

The split between the `max_charge_rate` setting and the charger's commands is the actions-versus-settings boundary applied to a charger. A ceiling is persistent and non-conflicting, so it is a setting; starting a session or handing timing to a strategy is a time-bound intent, so it is a command. The same boundary puts a battery's `safety_reserve` on settings and its `charge` on commands. See [canonical actions](/concepts/canonical-actions) and [capabilities](/concepts/capabilities).

## What next

<CardGroup cols={2}>
  <Card title="Subscribe to webhooks" icon="webhook" href="/guides/cookbook/subscribe-webhooks">
    Get push.completed events on charger state changes.
  </Card>

  <Card title="Handle conflicts" icon="triangle-alert" href="/guides/cookbook/handle-conflict">
    Resolve 409s when a charger write is already in flight.
  </Card>

  <Card title="Hold an HVAC device" icon="thermometer" href="/guides/cookbook/hvac-permanent-hold">
    The mirrored command pattern on a thermostat.
  </Card>

  <Card title="Canonical actions" icon="book-open" href="/concepts/canonical-actions">
    Where EV charger commands sit in the canonical model.
  </Card>
</CardGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Cap an EV Charger's Power",
"description": "Set an EV charger's maximum charging power as a device setting, then start and stop sessions with the charge and idle commands.",
"step": [
{
  "@type": "HowToStep",
  "name": "Step 1: Read the current state",
  "position": 1
},
{
  "@type": "HowToStep",
  "name": "Step 2: Cap the power",
  "position": 2
},
{
  "@type": "HowToStep",
  "name": "Step 3: Verify the cap applied",
  "position": 3
}
]
})}}
/>
