Canonical envelope for every rejection. Code taxonomy across 400, 401, 403, 404, 409, 422, 500, and 503, plus when to retry and when to fix the body.
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.
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.
Key is unrecognised, malformed, revoked, or soft-deleted.
EXPIRED_TOKEN
401
Key has passed its expiry.
RATE_LIMIT_EXCEEDED
429
Key has exceeded its rate ceiling.
INSUFFICIENT_PERMISSIONS
403
Key lacks the required permission.
LIVE_ACCESS_DISABLED
403
Live 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.
A 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_ERROR
400
A 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.
A canonical command (mode) is not in the device’s commands map. Body carries details.deviceCapabilities.supportedModes.
UNSUPPORTED_PARAMETER
422
A parameter is not declared for the resolved command. Body carries details.unsupportedParameters[] and deviceCapabilities.supportedParameters.
UNSUPPORTED_UNIT
422
A parameter unit is not declared as valid for this device. Body carries details: { parameter, providedUnit, supportedUnits }.
PARAMETER_OUT_OF_RANGE
422
A parameter value is outside the declared min/max bounds. Body carries details: { parameter, value, min, max, unit }.
EXECUTION_NOT_SUPPORTED
422
The request shape (immediate, scheduled, or windowed) is not in the mode’s execution array. Body carries details: { requestedExecution, supportedExecution }.
COMMAND_NOT_SUPPORTED
422
Live control for the device type is not yet available. Sandbox runs the full surface; the live path is gated.
STRATEGY_NOT_SUPPORTED
422
The requested onConflict strategy is not declared by the device. Body carries details: { requestedStrategy, supportedStrategies }.
DIRECT_ACTION_UNSUPPORTED
422
The device accepts no imperative push commands (native-scheduler-only).
START_IN_PAST
422
The start value is in the past.
START_OUT_OF_RANGE
422
The start value is more than 30 days in the future.
START_NONEXISTENT_WALL_CLOCK
422
The start wall-clock falls inside a DST spring-forward gap (the local clock jumps over it).
INVALID_TIME_WINDOW
422
start or end violates the time-window contract. Body carries details.reason naming the specific issue.
UNSUPPORTED_SETTING
422
Setting key is not declared for this device.
READ_ONLY_SETTING
422
Setting key is declared but read-only.
INVALID_SETTING_UNIT
422
Setting value’s unit does not match the unit declared for that setting.
INVALID_SETTING_VALUE
422
Setting value is the wrong type for that setting (e.g. a string where a number is expected).
A 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_EXECUTION
409
The 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_ACTIVE
409
Device-side scheduler conflicts with the requested action. Body carries details.reason and details.recoveryStrategies.
SCHEDULER_FULL
409
Merged schedule would exceed the OEM’s group limit. Body carries details.existingGroupCount and details.maxGroupCount.
ACTION_NOT_CANCELLABLE
409
Cancel request for an action already in acknowledged, completed, failed, or cancelled.
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.
Unexpected fault. Includes requestId for support correlation.
NOT_IMPLEMENTED
501
The 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_UNAVAILABLE
503
Transient 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.
No. The request is malformed or non-canonical. Fix the body.
401 / 403
No. 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.
422
No. 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.
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.
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).
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.
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.