error.code, not on message, and you will not see drift.
The envelope is the standard one. The code rides at error.code, the status is the HTTP status, the message at error.message is the line below, and error.details carries any structured context the platform forwarded from the device’s response.
Authentication and account
These codes appear when the manufacturer rejects the stored credentials for the linked account. Re-link the device through Link UI to capture a fresh credential.Device discovery
These codes name a problem locating the device on the manufacturer’s side.Command and parameter
These codes name a request the manufacturer would not run. The request body needs to change, not the timing.State conflict
These codes are 409s. The request is well-formed but the device is in a state that overrides or blocks it. Most are resolvable withonConflict: cancel_and_replace on the next push.
Transport and capacity
These codes are transient. The device manufacturer is reachable but is briefly unable to accept the request. Retry with exponential backoff.Platform
These codes are raised by the platform rather than forwarded from the manufacturer, though the fault behind one may still be the manufacturer’s. Most appear only on failed action records, delivered as theerrorCode on a push.failed webhook and readable from GET /actions/{actionId}. OEM_CIRCUIT_OPEN can also come back live on a push response when the platform has stopped calling the manufacturer.
What came back
INVALID_OEM_RESPONSE carries a details.fields array naming each reading that
failed and why, so you can see whether the manufacturer sent something
unrecognised or stopped sending it at all. received is present only when there
was a value, and is truncated.
The
message is identical for every occurrence, so match on code and read
details.fields for the specifics.
Readings the manufacturer sends that are not part of the canonical device
state never cause this error. They are simply not read.
When to retry
The platform never retries push commands on the manufacturer’s behalf. A command that runs minutes later is usually worse than one that fails, so the caller decides whether to resubmit, with what, and when.
Frequently asked questions
Why is the error message the same regardless of which manufacturer rejected the request?
The error message is API-owned. The platform translates the manufacturer’s response into one of the canonical codes on this page, and the message is fixed per code. This keeps customer-facing prose stable and avoids leaking manufacturer-internal language (codes, hostnames, product names) into the response. The manufacturer’s original text is retained in platform logs for diagnostics and is tied to the response throughmeta.requestId.
How do I tell a transient failure apart from a fatal one?
By the code, not by the HTTP status alone. Transport codes (NETWORK_ERROR, RATE_LIMITED, SERVICE_UNAVAILABLE, TIMEOUT) and DEVICE_OFFLINE are retryable. Auth and command codes are not retryable as-is: the credential needs refreshing or the request needs changing. State-conflict codes are resolvable with onConflict or by clearing the controlling state on the device. Treat UNKNOWN_ERROR as an unclassified manufacturer response: retry once and escalate if it persists.
What lives in error.details?
Structured context the platform forwarded from the manufacturer’s response, plus any platform-side context relevant to the rejection. For SCHEDULER_ACTIVE on a windowed push, that includes reason, description, overlappingGroups, existingGroupCount, recoveryStrategies, and detectedAt. For RATE_LIMITED, it may include a hint at the manufacturer’s reported retry window. For command codes, it typically carries the offending parameter and the device’s supported subset. The details shape is stable per code, so an integration can branch on it.
What if I receive a code that is not on this page?
The platform routes new manufacturer codes throughUNKNOWN_ERROR until they have a stable canonical mapping, so callers never see a raw manufacturer code. If you see UNKNOWN_ERROR against a request that should have succeeded, surface meta.requestId and the path and contact support; that pair lets the platform team trace the request and decide whether the response deserves its own code.
Related
Error envelope
The full code taxonomy, including request-validation and platform codes.
Conflict resolution
SCHEDULER_ACTIVE and onConflict in depth.
Handle a 409 conflict
Worked recovery patterns in code.
Battery cheat sheet
The canonical battery vocabulary in one page.