Skip to main content
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, and will work against the Execution MCP, 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:
{
  "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.balanced": { "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 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:
{ "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.balanced, 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:
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.
{
  "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 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 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

Dynamic UI rendering

How the same envelope drives generative UI. Shape-to-primitive mapping, streaming placeholders.

Confirmation gates

Why destructive tool calls do not free-fire from the model.

Capabilities

The presence-based capability contract in full.

Error envelope

The full code taxonomy and the details shapes the agent can act on.