> ## 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.

# What to expect

> Canonical vocabulary not behaviour. Self-documenting Quantity requests. Capability-driven design. Honest API with no hidden defaults.

The Amps API is built so any caller, whether a human developer or an AI agent, can read one response, understand what is possible, and construct a valid command without reading the docs first. The GET response describes the device. The POST request mirrors that structure with values filled in.

## Canonical vocabulary, not canonical behaviour

The API normalises vocabulary, not behaviour. `charge` means "the battery should be gaining energy". How that happens varies by OEM. The verb is universal; the underlying implementation is specific. The [capabilities](/concepts/capabilities) response on a specific device tells you what to expect there.

## Self-documenting requests

Every numeric parameter is a `Quantity`: `{ value, unit }`. The unit travels with the value.

```json theme={null}
{ "value": 80, "unit": "percent" }
{ "value": 3.5, "unit": "kw" }
```

This eliminates silent misinterpretation. The contract is explicit in every request. No `powerKw` vs `powerPercent` ambiguity, no mutual exclusivity validation, no docs lookup to know what the number represents. Send a unit the device does not accept and the API rejects with `UNSUPPORTED_UNIT`, naming the units that do work.

## Developer-accessible naming

Canonical command and parameter names are human-readable and uniform across every supported device:

| Canonical              | Meaning                                    |
| ---------------------- | ------------------------------------------ |
| `charge` / `discharge` | Direct imperatives.                        |
| `auto.balance`         | Self-consumption mode.                     |
| `auto.export`          | Grid-export mode.                          |
| `auto.reserve`         | Backup mode.                               |
| `target`               | An SOC bound for `charge` or `discharge`.  |
| `reserve`              | An SOC floor preserved during the command. |

## Capability-driven design

Capability is declared by presence. A command, parameter, or setting present in the device's capabilities response is supported; absent means rejected. There is no `supported: boolean` field. An empty `parameters: {}` means "valid command, no parameters". The structure you read is the structure you write back: `commands.charge.parameters.power` on the read maps directly to `action.parameters.power` on the write. This shape makes agentic workflows mechanical. An AI agent reads the device, picks a command, builds parameters from the constraint bounds, and posts. No hardcoded OEM knowledge required.

See [capabilities](/concepts/capabilities) for the full response shape.

## Honest by default

What you push is what the device receives. Amps does not paper over OEM limits with hidden defaults. Invoke a windowed-only mode without `start` and `end` and the API rejects with `EXECUTION_NOT_SUPPORTED`. No silent 24-hour covering window faking an immediate execution.

The same rule applies to settings writes. When an OEM treats omitted fields as "revert to defaults", Amps preserves your other settings automatically. You only send the field you want to change; nothing else is silently rewritten.

When Amps does not know how to honour a request, it surfaces the gap rather than guessing. The 422 carries `details` rich enough to recover from in one round trip.

## Per-mode granularity

Scheduling support is per-mode, not per-device. A device may accept `auto.balance` immediately but require `charge` to be windowed. Each command on the device declares an `execution` array drawn from `{ "immediate", "scheduled", "windowed" }`. Send a request shape that the array does not allow and the API returns 422 `EXECUTION_NOT_SUPPORTED` with `details: { requestedExecution, supportedExecution }`. Three tokens name three valid request shapes; the API rejects the call when the device cannot run the shape you asked for.

## Actions versus settings

Two surfaces exist on every controllable device:

| Surface                                  | Question                              | Properties                                    |
| ---------------------------------------- | ------------------------------------- | --------------------------------------------- |
| Actions (`POST /battery/{id}`)           | What should the device do?            | Time-boundable, conflict-able, audited.       |
| Settings (`POST /battery/{id}/settings`) | Within what bounds should it operate? | Persistent, non-conflicting, fire-and-forget. |

The litmus test: "does this make sense with a time window?" You can say "charge from 6pm to 10pm". You would never say "set the safety reserve to 10% from 6pm to 10pm". If it is time-boundable, it is an action.

Mode changes route through the push path; settings route through the settings path. The same shape works across every supported device.

## One schema for every vendor

The canonical schema is the only surface. The same request shape works across every supported device, and your code never needs to know which OEM is on the other end. No per-vendor keys, no `vendorParameters[]` array, no OEM-specific parameters bolt onto the payload.

## Every field is honoured or rejected

Every field you supply is either honoured, validated, or rejected. Sending an unknown field returns 422. Sending a known-but-unsupported parameter returns 422 `UNSUPPORTED_PARAMETER` with the supported set in `details`. Sending a parameter outside its declared bounds returns 422 `PARAMETER_OUT_OF_RANGE`. There is no path where a field is accepted, ignored, and you believe it took effect.

## Read-write parity

The same shape that comes out of `GET /battery/{id}` is what goes into `POST /battery/{id}`. The self-consistency test: any field the read names, the write either accepts or rejects with a precise reason. New concerns (settings, schedules, capabilities) get a new top-level field on the read and a new endpoint for the write. No retrofitting. No overloaded endpoints. Every concern has its own read field and its own write surface.

## Frequently asked questions

### Why does every parameter carry a unit?

The unit travels with the value to eliminate silent misinterpretation. Every numeric parameter is a `Quantity`: `{ value, unit }`. There is no `powerKw` vs `powerPercent` ambiguity, no docs lookup to know what `80` means. Send a unit a device does not accept and the API rejects with 422 `UNSUPPORTED_UNIT`, naming the units that do work. The contract is explicit in every request.

### Does Amps normalise OEM behaviour?

No. The API normalises vocabulary, not behaviour. `charge` means "the battery should be gaining energy"; how that happens varies by OEM. Each device declares what it accepts via [capabilities](/concepts/capabilities), and the read tells you what to expect there.

### Why are auto modes the same across OEMs?

Because they name intent at the protocol level, not implementation. `auto.balance` is the device's self-managing self-consumption mode under a shared name; `auto.reserve` is its backup mode; `auto.export` is its grid-export mode. The client sends the same request shape across every supported device.

### Are there per-vendor escape hatches?

No. There is no `vendorParameters[]` array and no OEM-specific keys on action payloads. The same request shape works across every supported device, and your code never needs to know which OEM is on the other end.

### What happens to fields the API does not recognise?

It returns 422. Every field a client supplies is either honoured, validated, or rejected. Sending an unknown field returns 422. Sending a known-but-unsupported parameter returns 422 `UNSUPPORTED_PARAMETER` with the supported set in `details`. Sending a value outside its declared bounds returns 422 `PARAMETER_OUT_OF_RANGE`. There is no path where a field is accepted, ignored, and you believe it took effect.

## Related concepts

<CardGroup cols={2}>
  <Card title="Canonical Actions" icon="zap" href="/concepts/canonical-actions">
    The action shape these principles produce.
  </Card>

  <Card title="Capabilities" icon="list-checks" href="/concepts/capabilities">
    How devices declare what they accept.
  </Card>

  <Card title="How Amps works" icon="layers" href="/concepts/architecture">
    Device control, strategy, and routing across a single API.
  </Card>

  <Card title="Error Envelope" icon="circle-alert" href="/concepts/error-envelope">
    Honest rejections that tell you exactly what is wrong.
  </Card>
</CardGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: [
    {
      "@type": "Question",
      name: "Why does every parameter carry a unit?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "The unit travels with the value to eliminate silent misinterpretation. Every numeric parameter is a Quantity: { value, unit }. There is no powerKw vs powerPercent ambiguity, no docs lookup to know what 80 means. Send a unit a device does not accept and the API rejects with 422 UNSUPPORTED_UNIT, naming the units that do work. The contract is explicit in every request."
      }
    },
    {
      "@type": "Question",
      name: "Does Amps normalise OEM behaviour?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "No. The API normalises vocabulary, not behaviour. charge means 'the battery should be gaining energy'; how that happens varies by OEM. Each device declares what it accepts via capabilities, and the read tells you what to expect there."
      }
    },
    {
      "@type": "Question",
      name: "Why are auto modes the same across OEMs?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "Because they name intent at the protocol level, not implementation. auto.balance is the device's self-managing self-consumption mode under a shared name; auto.reserve is its backup mode; auto.export is its grid-export mode. The client sends the same request shape across every supported device."
      }
    },
    {
      "@type": "Question",
      name: "Are there per-vendor escape hatches?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "No. There is no vendorParameters[] array and no OEM-specific keys on action payloads. The same request shape works across every supported device, and your code never needs to know which OEM is on the other end."
      }
    },
    {
      "@type": "Question",
      name: "What happens to fields the API does not recognise?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "It returns 422. Every field a client supplies is either honoured, validated, or rejected. Sending an unknown field returns 422. Sending a known-but-unsupported parameter returns 422 UNSUPPORTED_PARAMETER with the supported set in details. Sending a value outside its declared bounds returns 422 PARAMETER_OUT_OF_RANGE. There is no path where a field is accepted, ignored, and you believe it took effect."
      }
    }
  ]
})
}}
/>
