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:code is a stable, machine-readable identifier. message is the fixed API-owned explanation for that code. details is an optional object with structured data you can act on; safe route-specific copy is preserved under details.description. Errors always carry a code. Generic 500-class failures use INTERNAL_ERROR.
Recurring details fields: description, 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
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 can 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
404 rather than 403 to avoid leaking the existence of devices owned by other customers.
Malformed requests
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 stray or misplaced top-level key, a key
action does not declare, a unit that is not a canonical unit, a parameter of the wrong type, a malformed start, a body that is not JSON at all. 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. details.description explains that the 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 what the device does offer so you can recover.
An unrecognised action.command is not a 400. When the command string is the only thing wrong with the body, it comes back as 422 DEVICE_NOT_CAPABLE carrying details.deviceCapabilities.supportedModes and a details.description naming the mode, the same code and shape as a canonical command the device does not declare. The precondition is observable: add a second fault to the body, or leave command absent or numeric, and the field-precise 400 comes back instead. Placing start beside action rather than inside it is the 400 to picture, and details names the path to move the key to.
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
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.
What the device cannot do
One code answers every “this device does not offer that” rejection. Branch onerror.code. When details is present, read it for the refused value or the offered set. There is no value you can correct: the device does not have the capability, so ask it for something in the set it declares.
The checks below can include details.description and the structured fields listed in the table. Some immediate refusals carry only the code and message. Read each field by presence. These details belong to the immediate response. A refusal read back through GET /actions/{actionId}, the lastAction embed, or a webhook carries errorCode and errorMessage and nothing else.
Conflicts
See conflict resolution for the full model.
Device errors
A second family of rejections describes the device, not the shape of your request. The API raises some of them on the device’s behalf, from the capability map that device publishes. The manufacturer raises the rest. 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.
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
503 also covers transient internal unavailability. Retry with backoff.
NOT_IMPLEMENTED is not retryable.
See capability refusals and temporary pauses for the distinction.
When to retry
Recovery patterns
Most 422s are solvable from the response alone. Thedetails payload carries what the device does accept, so the agent or developer can re-derive a valid request without a second GET.
Which recovery fields a DEVICE_NOT_CAPABLE carries depends on what it refused, and the table above says which. An undeclared mode or parameter carries a deviceCapabilities snapshot, so you can rebuild the request directly. An unsupported request shape or conflict strategy carries both what you sent and what the device supports, in the same vocabulary, so you pick a valid one straight away. A setting that is declared but read-only carries details.setting alone, because no value makes that key writable through the public API. Two cases carry no alternative because none exists: a device that accepts no imperative push, and a charger whose manufacturer keeps no session history. Both carry details.description naming the missing capability, and the right answer there is to stop rather than to retry.
details belongs to the synchronous HTTP response. A push accepted for later execution and refused at the manufacturer reports through the action record instead, and GET /actions/{actionId}, the lastAction embed, and the webhook payload carry errorCode and errorMessage only.
Fields are never silently discarded. Where the unknown field sits decides the status. An unrecognised key in the push envelope, or inside action, is a 400 INVALID_REQUEST_BODY, and details names the offending key plus, where the key belongs inside action, the path to move it to. An unrecognised key inside action.parameters, or an unrecognised key in a settings body, is a 422 DEVICE_NOT_CAPABLE carrying what the device does declare: unsupportedParameters with deviceCapabilities.supportedParameters for a parameter, availableSettings for a setting. 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 withsuccess: 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 unrecognised key besideaction, or inside action itself, returns 400 INVALID_REQUEST_BODY naming the key. Sending an unrecognised key inside action.parameters, or an unrecognised setting key, returns 422 DEVICE_NOT_CAPABLE with the supported set in details. 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.
Related concepts
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.