---
title: Error Handling
slug: api-docs/error-handling
docTags: 
createdAt: 2026-05-11T22:18:16.436Z
---

# Error Handling

How Litmus APIs report errors and what to do about them.

## REST: HTTP status codes

Standard codes, used as you'd expect:

| Code      | Meaning          | Common cause                                                 |
| --------- | ---------------- | ------------------------------------------------------------ |
| 200 / 201 | Success          | --                                                           |
| 204       | No content       | DELETE returned successfully                                 |
| 400       | Validation error | Bad body, missing required field                             |
| 401       | Unauthorized     | Token expired, wrong client\_id/secret, wrong header for LEM |
| 403       | Forbidden        | Token is valid but the user / client lacks permission        |
| 404       | Not found        | Wrong path prefix or unknown resource ID                     |
| 409       | Conflict         | Resumable upload session already open (run DELETE first)     |
| 5xx       | Server error     | Check edge / LEM logs                                        |

Error body shape:

```json
{
  "error": "<short code>",
  "message": "<human readable>"
}
```

## GraphQL: 200 with errors array

GraphQL endpoints (`/devicehub/v2`, `/digitaltwins/v2`, `/cc/v2`, `/opcua/v2`, `/mqtt/gql`) return HTTP 200 even on failure. Errors live in the body:

```json
{
  "data": null,
  "errors": [
    {
      "message": "Driver not found: 12345",
      "path": ["CreateDevice"],
      "extensions": { "code": "NOT_FOUND" }
    }
  ]
}
```

**Always check&#x20;**`body.errors`**&#x20;before reading&#x20;**`body.data` - a non-null `errors` array means the operation failed even though the HTTP status is 200.

Some GraphQL responses return partial data alongside errors (e.g. one entry in a list failed). The contract is: data is what succeeded; errors lists what didn't.

## LEM async-task: per-subtask status

LEM async-task subtasks each carry a `status`:

| Status        | Meaning                                     |
| ------------- | ------------------------------------------- |
| `PENDING`     | Queued, not started                         |
| `IN_PROGRESS` | Running                                     |
| `SUCCESS`     | Done OK                                     |
| `FAILED`      | Done with error; check `errorMessage` field |

A parent task can have some subtasks succeed and some fail. Inspect each subtask, don't aggregate.

## Common 401 traps

| Product          | Header                                           | Wrong header symptom                                      |
| ---------------- | ------------------------------------------------ | --------------------------------------------------------- |
| LE               | `Authorization: Bearer <token>`                  | 401 with "invalid token"                                  |
| LEM `/api/v1/`   | `X-AuthToken: <token>`                           | 401 - using `Bearer` returns "missing token"              |
| LEM `/admin/v1/` | `X-AuthToken: <token>`                           | Same as above                                             |
| LEM `/mpcs/`     | `Authorization: <token>` (no `Bearer` prefix)    | 401 if you add `Bearer` or use `X-AuthToken`              |
| LUNS             | `Authorization: Bearer <token>` (Keycloak token) | 401 if you use the LE token instead of the UNS-issued one |

## Retries

- **Idempotent ops** (GET, PUT, DELETE on a specific ID): safe to retry on 5xx and timeout.
- **POST&#x20;**`CreateX`: not safe to retry blindly - may create duplicates. Either retry with a client-side deduplication key or verify the resource via List before retrying.
- **Async-task launch**: 5xx during step 3 = retry safe (no taskId returned). 5xx during polling = retry safe.
- **Resumable uploads**: if step 2 (PUT bytes) fails, run step 0 (DELETE) and start over - the partial bytes are discarded.
