> ## Documentation Index
> Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Device error codes

> Canonical error codes the API returns when a device manufacturer rejects a push, pull, or settings request. Stable code, HTTP status, API-owned message, and what to do.

When a push, pull, or settings request cannot be honoured, the API returns a stable canonical code, a fixed HTTP status, and a customer-safe message. Most codes on this page name a rejection from the device manufacturer; a smaller group at the end name a rejection from the platform itself. Every message is API-owned, so the manufacturer's raw text is never surfaced and the wording is the same whether the rejection comes from one manufacturer or another. Branch on `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.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "DEVICE_OFFLINE",
    "message": "The device is currently offline at the manufacturer.",
    "details": {}
  },
  "meta": {
    "requestId": "req_8a2Bf3kP",
    "timestamp": "2026-06-01T10:30:00.000Z",
    "path": "/battery/device_abc123",
    "latencyMs": 14
  }
}
```

## 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.

| Code                          | HTTP | Message                                                        | What to do                                                                                                           |
| ----------------------------- | ---- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `INVALID_CREDENTIALS`         | 401  | The device manufacturer rejected the supplied credentials.     | Send the end user back through Link UI to re-authenticate.                                                           |
| `INVALID_API_KEY`             | 401  | The device manufacturer rejected the supplied API key.         | The end user's manufacturer key has been revoked or rotated. Capture a new one via Link UI.                          |
| `INVALID_MFA_CODE`            | 403  | The multi-factor authentication code was not accepted.         | The code expired or was wrong. Restart the linking flow.                                                             |
| `MFA_REQUIRED`                | 403  | The device manufacturer requires multi-factor authentication.  | Restart the linking flow so the end user can complete MFA.                                                           |
| `ACCOUNT_LOCKED`              | 403  | The manufacturer account is locked.                            | The end user must unlock the account directly with the manufacturer before re-linking.                               |
| `UNSUPPORTED_CREDENTIAL_TYPE` | 400  | The supplied credential type is not supported for this device. | The credential captured during linking does not match what this manufacturer accepts. Re-run Link UI for the device. |
| `UNSUPPORTED_AUTH_PATH`       | 400  | The requested authentication method is not supported.          | The auth method the link flow tried is not enabled for this manufacturer. Re-run Link UI.                            |
| `CREDENTIAL_NOT_FOUND`        | 404  | No stored credential was found for this device.                | The link record is missing. Re-link the device through Link UI.                                                      |

## Device discovery

These codes name a problem locating the device on the manufacturer's side.

| Code                  | HTTP | Message                                                | What to do                                                                                                                       |
| --------------------- | ---- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `DEVICE_NOT_FOUND`    | 404  | No matching device was found for the supplied details. | The manufacturer no longer recognises this device, often because it was removed from the end user's account. Re-link to refresh. |
| `NO_DEVICES_FOUND`    | 404  | No devices were found on the manufacturer account.     | The linked account has no devices the manufacturer is willing to share. Check the end user's manufacturer app, then re-link.     |
| `DEVICE_OFFLINE`      | 410  | The device is currently offline at the manufacturer.   | Transient. The device has lost its connection to the manufacturer cloud. Retry once the device is back online.                   |
| `DEVICE_UNAUTHORIZED` | 403  | The account is not authorised to control this device.  | The end user can read the device but cannot send commands. They must grant control in the manufacturer app, then re-link.        |

## Command and parameter

These codes name a request the manufacturer would not run. The request body needs to change, not the timing.

| Code                      | HTTP | Message                                                                  | What to do                                                                                                                                             |
| ------------------------- | ---- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `COMMAND_FAILED`          | 400  | The device manufacturer could not complete the command.                  | The manufacturer accepted the request and then could not carry it out. Inspect the action and retry once any condition the device flagged has cleared. |
| `COMMAND_NOT_SUPPORTED`   | 422  | The device manufacturer does not support this command.                   | This command is not available on the device. Pick another command from the device's `commands` map.                                                    |
| `BIND_NOT_SUPPORTED`      | 400  | The device manufacturer does not support linking this device.            | The end user's device cannot be linked to a third-party API. There is no workaround inside Amps.                                                       |
| `INVALID_PARAMETERS`      | 400  | One or more request parameters were invalid.                             | A request parameter failed the manufacturer's own validation. Fix the value and resubmit.                                                              |
| `INVALID_OEM_PARAMETERS`  | 400  | One or more request parameters were rejected by the device manufacturer. | The manufacturer rejected a value that passed the API's own validation. Check the device's capability bounds and resubmit.                             |
| `EXECUTION_NOT_SUPPORTED` | 422  | The requested execution mode is not supported for this device.           | The request shape (immediate, scheduled, or windowed) is not in the command's `execution` array. Pick a supported shape.                               |
| `INVALID_TIME_WINDOW`     | 422  | The requested time window is invalid.                                    | The `start`/`end` pair violates the time-window contract. Inspect `error.details.reason` for the specific issue.                                       |

## 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 with `onConflict: cancel_and_replace` on the next push.

| Code               | HTTP | Message                                                                                                       | What to do                                                                                                                                                                                                                                      |
| ------------------ | ---- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SCHEDULER_ACTIVE` | 409  | A schedule is currently active on the device and must be cleared first.                                       | Resubmit with `onConflict: cancel_and_replace` to clear the conflicting schedule rows. `error.details.recoveryStrategies` lists the strategies the platform supports for this device. See [conflict resolution](/concepts/conflict-resolution). |
| `SCHEDULER_FULL`   | 409  | The device schedule is full.                                                                                  | The merged schedule would exceed the manufacturer's slot limit. Cancel an existing schedule or ask the end user to clear rows from the manufacturer app.                                                                                        |
| `MODE_OVERRIDDEN`  | 409  | The device is in a state that overrides the requested mode. Clear the controlling schedule or override first. | Another active mode or schedule on the device is dominant. Clear it before the command will take effect.                                                                                                                                        |
| `VPP_LOCKED`       | 409  | The device is currently locked by another control programme and cannot accept this command.                   | The device has been enlisted in a manufacturer-side virtual-power-plant or aggregator programme that takes precedence. The end user must opt out at the manufacturer before Amps can write.                                                     |

## Transport and capacity

These codes are transient. The device manufacturer is reachable but is briefly unable to accept the request. Retry with exponential backoff.

| Code                  | HTTP | Message                                                                      | What to do                                                                                                                                                                |
| --------------------- | ---- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NETWORK_ERROR`       | 502  | The device manufacturer could not be reached.                                | A transport-level failure between the platform and the manufacturer cloud. Retry.                                                                                         |
| `RATE_LIMITED`        | 429  | The device manufacturer is rate limiting requests. Please try again shortly. | The manufacturer's per-account or per-device limit was hit. Back off and retry.                                                                                           |
| `SERVICE_UNAVAILABLE` | 503  | The device manufacturer service is temporarily unavailable.                  | The manufacturer cloud is down or refusing requests. Retry with backoff.                                                                                                  |
| `TIMEOUT`             | 504  | The device manufacturer did not respond in time.                             | The manufacturer accepted the request but did not respond before the API's timeout. Retry.                                                                                |
| `SIMULATED_FAILURE`   | 503  | Simulated failure (sandbox).                                                 | Reached only in sandbox via `sandbox: { result: "failed", errorCode }`. Used to exercise error-handling without touching a real device.                                   |
| `UNKNOWN_ERROR`       | 500  | The device manufacturer rejected the request.                                | The manufacturer responded with a state the platform does not yet have a stable code for. `meta.requestId` ties the request to platform logs; surface it when escalating. |

## Platform

These codes originate inside the platform rather than the manufacturer. Most appear only on failed action records, delivered as the `errorCode` 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.

| Code                       | HTTP | Message                                                                                                           | What to do                                                                                                                                                    |
| -------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OEM_CIRCUIT_OPEN`         | 503  | The platform is temporarily not contacting the device manufacturer (a protective circuit is open). Retry shortly. | A protective circuit is open against this manufacturer. Wait and retry; the circuit closes automatically once the underlying condition clears.                |
| `COMMAND_NOT_APPLIED`      | 409  | The device did not apply the requested command.                                                                   | Read-back completed without observing the requested state. Inspect the action and device state before submitting another command.                             |
| `STALE_ACTION`             | 410  | The action expired before it could be processed and was cleaned up.                                               | The action was created but not dispatched within the platform's stale-action window. Re-submit if you still want it.                                          |
| `DEFERRED_SCHEDULE_FAILED` | 500  | The action could not be scheduled for deferred execution.                                                         | A scheduled action could not be placed on the platform's execution timer. Re-submit the action.                                                               |
| `UNROUTABLE_ACTION_TYPE`   | 500  | The action type is not supported for this device.                                                                 | The action type does not resolve to an execution path for this device. Check the device's `commands` or `settings` map and re-submit with a supported action. |

## When to retry

| Class                                                                                  | Retry?                                                                        |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Transport (`NETWORK_ERROR`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`, `TIMEOUT`)          | Yes, with exponential backoff.                                                |
| Device offline (`DEVICE_OFFLINE`)                                                      | Yes, after a delay. The device is briefly unreachable.                        |
| State conflict (`SCHEDULER_ACTIVE`, `MODE_OVERRIDDEN`, `VPP_LOCKED`)                   | Resolve via `onConflict` or by clearing the controlling state, then resubmit. |
| Auth (`INVALID_CREDENTIALS`, `MFA_REQUIRED`, `ACCOUNT_LOCKED`, `CREDENTIAL_NOT_FOUND`) | No. Re-link the device.                                                       |
| Command (`COMMAND_NOT_SUPPORTED`, `INVALID_PARAMETERS`, `EXECUTION_NOT_SUPPORTED`)     | No. Fix the request body.                                                     |
| Circuit open (`OEM_CIRCUIT_OPEN`)                                                      | Yes, after a delay. The platform closes the circuit automatically.            |
| Expired (`STALE_ACTION`)                                                               | No. Re-submit the action fresh.                                               |
| Deferred scheduling failed (`DEFERRED_SCHEDULE_FAILED`)                                | Re-submit the action.                                                         |
| `UNROUTABLE_ACTION_TYPE`, `UNKNOWN_ERROR`                                              | Once, then escalate with the `requestId`.                                     |

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 through `meta.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 through `UNKNOWN_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

<CardGroup cols={2}>
  <Card title="Error envelope" icon="circle-alert" href="/concepts/error-envelope">
    The full code taxonomy, including request-validation and platform codes.
  </Card>

  <Card title="Conflict resolution" icon="git-merge" href="/concepts/conflict-resolution">
    SCHEDULER\_ACTIVE and onConflict in depth.
  </Card>

  <Card title="Handle a 409 conflict" icon="x" href="/guides/cookbook/handle-conflict">
    Worked recovery patterns in code.
  </Card>

  <Card title="Battery cheat sheet" icon="battery-charging" href="/reference/battery-cheat-sheet">
    The canonical battery vocabulary in one page.
  </Card>
</CardGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: [
    {
      "@type": "Question",
      name: "Why is the error message the same regardless of which manufacturer rejected the request?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "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 through meta.requestId."
      }
    },
    {
      "@type": "Question",
      name: "How do I tell a transient failure apart from a fatal one?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "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."
      }
    },
    {
      "@type": "Question",
      name: "What lives in error.details?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "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."
      }
    },
    {
      "@type": "Question",
      name: "What if I receive a code that is not on this page?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "The platform routes new manufacturer codes through UNKNOWN_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."
      }
    }
  ]
})
}}
/>
