Skip to main content
Every Amps response uses the same envelope. Successes carry a data field, failures carry an error field, and both carry meta for tracing and pagination. The shape is predictable across every endpoint, every device type, every status code.

The envelope

A successful response:
{
  "success": true,
  "data": { "id": "device_abc123", "state": "..." },
  "meta": {
    "requestId": "req_8a2Bf3kP",
    "environment": "sandbox",
    "timestamp": "2026-06-01T10:30:00.000Z",
    "latencyMs": 12
  }
}
A failed response:
{
  "success": false,
  "error": {
    "code": "UNSUPPORTED_UNIT",
    "message": "Parameter `power` unit `percent` is not supported. Expected `kw`.",
    "details": {
      "parameter": "power",
      "providedUnit": "percent",
      "supportedUnits": ["kw"]
    }
  },
  "meta": {
    "requestId": "req_8a2Bf3kP",
    "timestamp": "2026-06-01T10:30:00.000Z",
    "path": "/battery/device_abc123",
    "latencyMs": 14
  }
}
code is a stable, machine-readable identifier. message is a human-readable explanation. details is an optional object with structured data you can act on. Errors always carry a code. Generic 500-class failures use INTERNAL_ERROR. Recurring details fields: unsupportedParameters[], conflictingActionIds[], deviceCapabilities, requestedExecution, supportedExecution, reason, and the parameter-bounds fields (parameter, value, min, max, unit, providedUnit, supportedUnits). Each rides along only when it applies to the failure. These let you fix the request in a single round trip. deviceCapabilities carries the supported subset (supportedModes, supportedParameters, or supportedActions) in the same vocabulary as the GET response, so you can re-derive a valid request without re-fetching the device.

Authentication

CodeHTTPTrigger
UNAUTHORIZED401No API key in the request.
INVALID_API_KEY401Key is unrecognised, malformed, revoked, or soft-deleted.
EXPIRED_TOKEN401Key has passed its expiry.
RATE_LIMIT_EXCEEDED429Key has exceeded its rate ceiling.
INSUFFICIENT_PERMISSIONS403Key lacks the required permission.
LIVE_ACCESS_DISABLED403Live key against a customer not enabled for live.
The platform rate-limits at the API-key layer, so rate-limit rejections come back on the 401 path rather than as a separate 429. A rapid burst against one key may currently surface as a plain 401 UNAUTHORIZED (“Authentication failed”) rather than RATE_LIMIT_EXCEEDED. If you are seeing 401s under load with a key you know is valid, back off and retry rather than treating the key as broken. See auth and environments for context.

Resource access

CodeHTTPTrigger
DEVICE_NOT_FOUND404Device does not exist or is not linked to your account in this environment.
404 rather than 403 to avoid leaking the existence of devices owned by other customers.

Malformed requests

CodeHTTPTrigger
INVALID_REQUEST_BODY400A parseable body whose contents fall outside the canonical vocabulary: a command or unit the vocabulary does not define, a parameter of the wrong type, or a settings body that is not a key-value map.
VALIDATION_ERROR400A query parameter is out of bounds (limit above 50 or non-numeric, negative offset, or an unrecognised state or type on GET /actions); the body carries details.fields keyed by parameter. Or a request body that is not valid JSON, caught at the request boundary before the controller (“Body is not valid JSON”).
A 400 means the request never made it to the device, because it was not a request the API could interpret. This is the boundary with a 422. A 400 fires when the input is not even valid canonical input: a command the vocabulary does not define, a unit that is not a canonical unit, a parameter of the wrong type. VALIDATION_ERROR is the 400 you hit first on a list GET: a query string outside its bounds, such as limit above 50 or non-numeric, or a negative offset, comes back with the offending fields under details.fields. The action-list endpoint GET /actions adds state and type filters to the same bounds check, so an unrecognised state or device type there is a VALIDATION_ERROR too. The same code covers a request body that never parses as JSON, caught at the request boundary before the controller sees it (“Body is not valid JSON”). A 422 fires when the input is well-formed canonical input that this specific device cannot satisfy, and it carries a deviceCapabilities snapshot so you can recover. Sending command: "explode" is a 400 INVALID_REQUEST_BODY; sending command: "charge" to a device that does not declare charge is a 422 UNSUPPORTED_MODE. The first is a coding error, the second is a device fact you can act on. A malformed start value lands here: a Z or +HH:MM offset, or a string that is neither a plant-local wall-clock ISO 8601 nor a relative duration (30m, 1.5h), comes back as 400 INVALID_REQUEST_BODY with the explanation under details.fields["action.start"]. Times are plant-local wall-clock without an offset. The 422 START_* codes above are semantic rejections (in the past, out of the 30-day horizon, in a DST gap); the format and offset checks fire ahead of them at the request boundary.

Action validation

CodeHTTPTrigger
UNSUPPORTED_MODE422A canonical command (mode) is not in the device’s commands map. Body carries details.deviceCapabilities.supportedModes.
UNSUPPORTED_PARAMETER422A parameter is not declared for the resolved command. Body carries details.unsupportedParameters[] and deviceCapabilities.supportedParameters.
UNSUPPORTED_UNIT422A parameter unit is not declared as valid for this device. Body carries details: { parameter, providedUnit, supportedUnits }.
PARAMETER_OUT_OF_RANGE422A parameter value is outside the declared min/max bounds. Body carries details: { parameter, value, min, max, unit }.
EXECUTION_NOT_SUPPORTED422The request shape (immediate, scheduled, or windowed) is not in the mode’s execution array. Body carries details: { requestedExecution, supportedExecution }.
COMMAND_NOT_SUPPORTED422Live control for the device type is not yet available. Sandbox runs the full surface; the live path is gated.
STRATEGY_NOT_SUPPORTED422The requested onConflict strategy is not declared by the device. Body carries details: { requestedStrategy, supportedStrategies }.
DIRECT_ACTION_UNSUPPORTED422The device accepts no imperative push commands (native-scheduler-only).
START_IN_PAST422The start value is in the past.
START_OUT_OF_RANGE422The start value is more than 30 days in the future.
START_NONEXISTENT_WALL_CLOCK422The start wall-clock falls inside a DST spring-forward gap (the local clock jumps over it).
INVALID_TIME_WINDOW422start or end violates the time-window contract. Body carries details.reason naming the specific issue.
UNSUPPORTED_SETTING422Setting key is not declared for this device.
READ_ONLY_SETTING422Setting key is declared but read-only.
INVALID_SETTING_UNIT422Setting value’s unit does not match the unit declared for that setting.
INVALID_SETTING_VALUE422Setting value is the wrong type for that setting (e.g. a string where a number is expected).
SETTING_OUT_OF_RANGE422Setting value is outside the declared bounds.
INVALID_TIME_WINDOW reasons: end_without_start, invalid_end_format, end_not_after_start, malformed_wall_clock, sub_minute_window_not_supported, window_must_not_span_midnight.

Conflicts

CodeHTTPTrigger
CONFLICT409A non-terminal action conflicts with the request and the right onConflict can resolve it: none was supplied (no_strategy_supplied), or queue_after has no window to queue behind (conflicting_action_not_windowed). Body carries details.conflictingActionIds[], details.reason, and strategies[] listing the strategy that would resolve it.
CONFLICT_IN_EXECUTION409The conflicting action is already in execution (acknowledged and dispatched, completion pending), so no strategy can displace it (reason: conflicting_action_in_progress). Body carries details.conflictingActionIds[]. Wait for it to complete or fail, then re-submit.
SCHEDULER_ACTIVE409Device-side scheduler conflicts with the requested action. Body carries details.reason and details.recoveryStrategies.
SCHEDULER_FULL409Merged schedule would exceed the OEM’s group limit. Body carries details.existingGroupCount and details.maxGroupCount.
ACTION_NOT_CANCELLABLE409Cancel request for an action already in acknowledged, completed, failed, or cancelled.
See conflict resolution for the full model.

Device manufacturer errors

A second family of rejections originates from the device manufacturer, not the API. Auth failures, command refusals, device-offline, rate limits, and transport outages all surface with a stable canonical code, a fixed HTTP status, and an API-owned, customer-safe message. The manufacturer’s raw text never reaches the response.
ClassExample codesTrigger
AuthenticationINVALID_CREDENTIALS, MFA_REQUIRED, ACCOUNT_LOCKED, CREDENTIAL_NOT_FOUNDThe manufacturer rejected the stored linked-account credential. Re-link the device.
DeviceDEVICE_OFFLINE (410), DEVICE_NOT_FOUND (404), DEVICE_UNAUTHORIZED (403)The device is missing, offline, or not controllable from this account.
CommandCOMMAND_NOT_SUPPORTED, INVALID_PARAMETERS, INVALID_OEM_PARAMETERS, UNKNOWN_SETTINGThe manufacturer refused the command or a parameter, after the API’s own validation passed.
State conflictSCHEDULER_ACTIVE (409), SCHEDULER_FULL (409), MODE_OVERRIDDEN (409), VPP_LOCKED (409)The device is in a state that overrides or blocks the request. Resolve via onConflict or clear the controlling state.
TransportRATE_LIMITED (429), NETWORK_ERROR (502), SERVICE_UNAVAILABLE (503), TIMEOUT (504)Transient. Retry with exponential backoff.
See the device error codes reference for every code, its status, its exact API-owned message, the details shape, and what to do.

Runtime and platform

CodeHTTPTrigger
INTERNAL_ERROR500Unexpected fault. Includes requestId for support correlation.
NOT_IMPLEMENTED501The surface is published ahead of its implementation. The schedule routes return NOT_IMPLEMENTED until the scheduler ships; the message names the specific surface (listing, retrieval, write, clear).
SERVICE_UNAVAILABLE503Transient unavailability. Safe to retry with exponential backoff.
503 also covers transient internal unavailability. Standard retry behaviour suffices. NOT_IMPLEMENTED is not retryable; the route is gated until the feature lands.

When to retry

ClassRetry?
5xx (INTERNAL_ERROR, SERVICE_UNAVAILABLE)Yes, with exponential backoff and jitter.
Rate limit (RATE_LIMIT_EXCEEDED)Yes, after a backoff.
400 (INVALID_REQUEST_BODY, VALIDATION_ERROR)No. The request is malformed or non-canonical. Fix the body.
401 / 403No. Fix auth or live-access enablement first.
404 (DEVICE_NOT_FOUND)No. The link is missing.
409 (CONFLICT, SCHEDULER_ACTIVE)Resolve via onConflict or wait for the conflict to terminate, then re-submit.
409 (CONFLICT_IN_EXECUTION)No strategy helps. Wait for the in-flight action to complete or fail, then re-submit.
422No. Fix the request body using details.
Push actions are not retried server-side on OEM rejection. Commands are time-sensitive, and running minutes later is usually worse than failing. You decide whether to re-submit, and with what.

Recovery patterns

A 422 is solvable from the response alone. The details payload carries the device’s current capability snapshot, so the agent or developer can re-derive a valid request without a second GET. UNSUPPORTED_MODE and UNSUPPORTED_PARAMETER carry a deviceCapabilities snapshot, so you can rebuild the request without a second GET. EXECUTION_NOT_SUPPORTED carries both what you sent and what the mode supports, in the same vocabulary, so you pick a valid shape directly. The error response is enough on its own to fix the call. Fields are never silently discarded. An unknown field returns 422. A known-but-unsupported field returns the corresponding 422. There is no path where a field is accepted, ignored, and you believe it took effect. meta.requestId ties a request to platform logs. Surface it in your error handler when escalating to support.

Frequently asked questions

What does a typical error response look like?

A JSON body with success: false, an error object with code and message (plus optional details), and a meta object with requestId, timestamp, path, and latencyMs. The shape is identical across every endpoint, device type, and status code. Successes use the same envelope with success: true and data; their meta carries requestId, environment (sandbox or live), timestamp, and latencyMs. The code is stable and machine-readable; the message is human-readable; details is structured data the caller can act on (e.g., unsupportedParameters[], conflictingActionIds[], a deviceCapabilities snapshot).

Which error codes are retryable?

5xx (INTERNAL_ERROR, SERVICE_UNAVAILABLE) and rate-limit rejections (RATE_LIMIT_EXCEEDED) are retryable with a backoff. 400 (INVALID_REQUEST_BODY, VALIDATION_ERROR) is not retryable: the request is malformed or non-canonical. 401 and 403 are not retryable: fix auth or live-access enablement first, though a 401 under heavy load can be a rate limit, so back off and retry once. 404 (DEVICE_NOT_FOUND) is not retryable: the link is missing. 409s are resolvable via onConflict. 422s are not retryable as-is: fix the request body using details. Push actions are never retried server-side on OEM rejection because commands are time-sensitive.

How do I distinguish a client error from a server error?

By status code class. 4xx codes name client errors: 400 (malformed or non-canonical body), 401 (auth), 403 (permissions), 404 (resource access), 409 (conflict), 422 (well-formed but device-rejected). 5xx codes name server-side faults: 500 (INTERNAL_ERROR), 503 (SERVICE_UNAVAILABLE). The body in either class follows the same envelope. When escalating to support, surface meta.requestId in your error handler; it ties the request to platform logs.

Why does the API never silently discard fields?

Because silent discarding hides bugs and breaks the contract. 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. Sending a value outside its bounds returns 422 PARAMETER_OUT_OF_RANGE. There is no path where the field is accepted, ignored, and the client believes the field took effect. Honest rejections are part of the API design contract.

Canonical Actions

The shape that produces validation errors.

Conflict Resolution

CONFLICT, SCHEDULER_ACTIVE deep dive.

Capabilities

The deviceCapabilities snapshot returned in rejections.

Auth and Environments

Authentication errors in detail.

Device Error Codes

Every code the device side can raise, with status, message, and retry guidance.