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

# Self-describing responses

> Every Amps response carries the contract for the next request. Capability introspection, typed Quantity envelopes, and 422s that return the device snapshot.

The Amps API is built so an agent never needs device-specific code. Every read response carries the contract for the next write. Every error envelope carries enough structure for the agent to fix its own request. The same shape that drives reasoning drives rendering.

This page is for integrators wiring an agent at the API layer or building a UI on top of it. The patterns work against the deployed REST API and the [Documentation MCP](/ai-tools/mcp/documentation), and will work against the [Execution MCP](/ai-tools/mcp/execution), which is coming soon.

## Capabilities ship with state

There is no separate `/capabilities` endpoint. A `GET /battery/{deviceId}` returns the device's current state and its full capability declaration in the same body:

```json theme={null}
{
  "success": true,
  "data": {
    "id": "device_abc123",
    "vendor": "example_vendor_a",
    "sync": { "available": true, "lastPulledAt": "2026-06-01T10:30:00.000Z" },
    "metadata": { "model": "Hybrid 5kWh", "source": "cache" },
    "state": { "status": "charging", "level": 50, "capacity": 10.4, "chargeRate": 2.4, "dischargeLimit": 10 },
    "conflictStrategies": ["cancel_and_replace", "queue_after"],
    "commands": {
      "charge": {
        "parameters": {
          "power":  { "unit": "kw",      "min": 0,  "max": 5.0 },
          "target": { "unit": "percent", "min": 10, "max": 100 }
        },
        "execution": ["immediate", "scheduled", "windowed"]
      },
      "auto.balance": { "parameters": {}, "execution": ["immediate", "scheduled"] }
    },
    "settings": {
      "discharge_floor": { "value": 10, "unit": "percent", "min": 0, "max": 100 }
    },
    "lastAction": null,
    "currentSchedule": null
  },
  "meta": { "requestId": "req_8a2Bf3kP", "environment": "live", "timestamp": "2026-06-01T10:30:00.000Z", "latencyMs": 12 }
}
```

The shape of `commands.charge.parameters.power` on the read maps directly to `action.parameters.power` on the write. One read tells the agent which commands fire, which parameters apply with which units and bounds, and which execution shapes (`immediate`, `scheduled`, `windowed`) the device can run.

See [capabilities](/concepts/capabilities) for the full presence-based contract: a key is present iff the device supports it. There is no `supported: boolean` middle state.

## The Quantity envelope

Every numeric parameter is a `Quantity`: an object with both `value` and `unit`. The unit travels with the value, so `80` is never ambiguous between percent, kilowatts, or amps:

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

Units are drawn from a fixed enum: `percent`, `kw`, `watts`, `kwh`, `amps`, `volts`, `hours`, `minutes`, `celsius`, `fahrenheit`. The enum is the shape that lets a UI component switch on unit at the type layer, so a numeric scalar with `unit: "percent"` renders as a state-of-charge ring while `unit: "kw"` renders as a power gauge with kilowatt scale. Same component shape, different visual output, no per-device branching.

## Build the next request from the read

The agent's loop:

1. `GET /battery/{deviceId}` (or `get_battery` via the Execution MCP)
2. Inspect `commands` to choose a verb (`charge`, `discharge`, `auto.balance`, etc.)
3. Inspect `commands.<verb>.parameters` to know which parameters apply, with what units and bounds
4. Inspect `commands.<verb>.execution` to choose `immediate`, `scheduled`, or `windowed`
5. Build the body and `POST /battery/{deviceId}`

A first cut in TypeScript:

```ts theme={null}
type Capability = {
  parameters: Record<string, { unit: string; min?: number; max?: number }>;
  execution: ("immediate" | "scheduled" | "windowed")[];
};

function buildChargeAction(cap: Capability, target = 80) {
  const targetParam = cap.parameters.target;
  const clampedTarget = Math.min(targetParam.max ?? 100, Math.max(targetParam.min ?? 0, target));
  return {
    action: {
      command: "charge",
      parameters: {
        target: { value: clampedTarget, unit: targetParam.unit },
      },
    },
  };
}
```

The agent does not know in advance whether a device accepts `target` in `percent` or in `kwh`. It reads, it builds. The same code works across every device family that follows the canonical model.

## Errors carry the contract

A 422 rejection ships a structured `details` block the agent can act on. It names exactly what failed (`unsupportedParameters`) and the device's supported set (`deviceCapabilities`), in the same canonical vocabulary the GET returns.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "UNSUPPORTED_PARAMETER",
    "message": "Parameter(s) not supported for mode `charge`: reserve.",
    "details": {
      "unsupportedParameters": ["reserve"],
      "deviceCapabilities": {
        "supportedParameters": ["power", "target"]
      }
    }
  },
  "meta": {
    "requestId": "req_8a2Bf3kP",
    "timestamp": "2026-06-01T10:30:00.000Z",
    "path": "/battery/device_abc123",
    "latencyMs": 14
  }
}
```

The agent drops the parameter named in `details.unsupportedParameters`, confirms the replacement against `details.deviceCapabilities.supportedParameters`, and retries. One round trip, no human intervention. The same pattern fires for `UNSUPPORTED_MODE` (which carries `deviceCapabilities.supportedModes`), `UNSUPPORTED_UNIT` (which carries `parameter`, `providedUnit`, `supportedUnits`), and `EXECUTION_NOT_SUPPORTED` (which carries `requestedExecution`, `supportedExecution`). See [error envelope](/concepts/error-envelope) for the full code list.

This is what makes the API safe for an agent to drive autonomously. The error envelope is not a dead end. It is the next read.

## Conflict envelopes are self-describing too

When a push collides with an active action and the request did not declare `onConflict`, the API returns 409 `CONFLICT` with the conflicting action IDs in `details.conflictingActionIds`. The agent reads, decides whether to `cancel_and_replace` or `queue_after`, and re-posts with `onConflict` set. Same pattern: rejection plus enough structure to fix the next call.

See [conflict resolution](/concepts/conflict-resolution) for the strategies and trade-offs.

## What this gives you

A single agent loop, one set of UI primitives, every device family in the canonical model. The agent's reasoning code does not branch per OEM. The UI components do not branch per OEM. Every device that joins the model gets the same tooling for free.

This is what "self-describing" delivers in practice. Not a marketing line. A shape choice that makes agents possible.

## What next

<CardGroup cols={2}>
  <Card title="Dynamic UI rendering" icon="layout-grid" href="/ai-tools/building-agent-uis/dynamic-ui-rendering">
    How the same envelope drives generative UI. Shape-to-primitive mapping, streaming placeholders.
  </Card>

  <Card title="Confirmation gates" icon="shield-check" href="/ai-tools/building-agent-uis/confirmation-gates">
    Why destructive tool calls do not free-fire from the model.
  </Card>

  <Card title="Capabilities" icon="list-checks" href="/concepts/capabilities">
    The presence-based capability contract in full.
  </Card>

  <Card title="Error envelope" icon="circle-x" href="/concepts/error-envelope">
    The full code taxonomy and the `details` shapes the agent can act on.
  </Card>
</CardGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Self-describing responses",
"description": "Every Amps response carries the contract for the next request.",
"datePublished": "2026-05-08",
"dateModified": "2026-05-08"
})}}
/>
