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

# Errors

> One envelope, stable codes, and worked examples in curl, TypeScript, and Python.

## The envelope

Every JSON API error has the same shape:

```json theme={null}
{
  "error": {
    "code": "SYNC_WEEKLY_LIMIT_REACHED",
    "message": "Cloud report limit reached. This scan stays local.",
    "requestId": "8f14e45f-ceea-467a-9f8b-2b6b2b6a1f2c",
    "details": {
      "weekly": { "used": 15, "limit": 15, "resetsAt": "2026-08-24T06:00:00.000Z" }
    }
  }
}
```

Branch on `code`. The `message` is written for a human and may change; `requestId` is a
fresh UUID per response, for correlating with server logs.

The complete code list, with HTTP statuses and what to do about each one, is in
[Error codes](/reference/error-codes).

## Handling errors

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    response=$(curl -s -w '\n%{http_code}' \
      http://localhost:3000/api/v1/sync-quota \
      -H "authorization: Bearer $MYRQEN_DEVICE_TOKEN")

    status=$(printf '%s' "$response" | tail -n1)
    body=$(printf '%s' "$response" | sed '$d')

    if [ "$status" -ge 400 ]; then
      printf '%s' "$body" | python3 -c 'import json,sys; e=json.load(sys.stdin)["error"]; print(e["code"], "-", e["message"])'
      exit 1
    fi

    printf '%s\n' "$body"
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts theme={null}
    interface ApiError {
      error: {
        code: string;
        message: string;
        requestId: string;
        details?: Record<string, unknown>;
      };
    }

    class MyrqenError extends Error {
      constructor(
        readonly code: string,
        message: string,
        readonly status: number,
        readonly requestId: string,
        readonly details?: Record<string, unknown>,
      ) {
        super(message);
        this.name = "MyrqenError";
      }
    }

    async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
      const response = await fetch(`http://localhost:3000${path}`, {
        ...init,
        headers: {
          accept: "application/json",
          authorization: `Bearer ${process.env.MYRQEN_DEVICE_TOKEN}`,
          ...(init.body ? { "content-type": "application/json" } : {}),
          ...init.headers,
        },
      });

      const text = await response.text();

      if (!response.ok) {
        let parsed: ApiError | null = null;
        try {
          parsed = JSON.parse(text) as ApiError;
        } catch {
          // Not every failure comes from the application layer.
        }
        throw new MyrqenError(
          parsed?.error.code ?? "MALFORMED",
          parsed?.error.message ?? `Request failed with status ${response.status}.`,
          response.status,
          parsed?.error.requestId ?? "",
          parsed?.error.details,
        );
      }

      return text.length === 0 ? (undefined as T) : (JSON.parse(text) as T);
    }
    ```

    Then branch on the code, not the status:

    ```ts theme={null}
    try {
      const quota = await call<{ eligible: boolean }>("/api/v1/sync-quota");
      console.log(quota.eligible ? "can sync" : "quota exhausted");
    } catch (error) {
      if (error instanceof MyrqenError) {
        switch (error.code) {
          case "SYNC_DAILY_LIMIT_REACHED":
          case "SYNC_WEEKLY_LIMIT_REACHED":
            console.log("Staying local for this scan.");
            break;
          case "DEVICE_REVOKED":
            console.log("Re-link this machine with `myrqen link`.");
            break;
          case "RATE_LIMITED":
            console.log("Back off and retry.");
            break;
          default:
            throw error;
        }
      } else {
        throw error;
      }
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os
    import httpx


    class MyrqenError(Exception):
        def __init__(self, code, message, status, request_id, details=None):
            super().__init__(message)
            self.code = code
            self.status = status
            self.request_id = request_id
            self.details = details or {}


    BASE_URL = "http://localhost:3000"


    def call(path, method="GET", json_body=None):
        response = httpx.request(
            method,
            f"{BASE_URL}{path}",
            json=json_body,
            headers={
                "accept": "application/json",
                "authorization": f"Bearer {os.environ['MYRQEN_DEVICE_TOKEN']}",
            },
            timeout=20.0,
        )

        if response.is_error:
            try:
                error = response.json()["error"]
            except Exception:
                raise MyrqenError(
                    "MALFORMED",
                    f"Request failed with status {response.status_code}.",
                    response.status_code,
                    "",
                )
            raise MyrqenError(
                error["code"],
                error["message"],
                response.status_code,
                error["requestId"],
                error.get("details"),
            )

        return response.json() if response.content else None


    try:
        quota = call("/api/v1/sync-quota")
        print("can sync" if quota["eligible"] else "quota exhausted")
    except MyrqenError as error:
        if error.code in ("SYNC_DAILY_LIMIT_REACHED", "SYNC_WEEKLY_LIMIT_REACHED"):
            print("Staying local for this scan.")
        elif error.code == "DEVICE_REVOKED":
            print("Re-link this machine with `myrqen link`.")
        else:
            raise
    ```
  </Tab>
</Tabs>

## Retry guidance

| Code                                                                                                        | Retry?                                                                            |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `RATE_LIMITED`                                                                                              | Yes, after a back-off.                                                            |
| `INTERNAL`                                                                                                  | Yes, once, with a back-off.                                                       |
| `SYNC_DAILY_LIMIT_REACHED`, `SYNC_WEEKLY_LIMIT_REACHED`                                                     | Not until the window resets — `details` carries `resetsAt`.                       |
| `DEVICE_LINK_NOT_APPROVED`                                                                                  | Yes, that is the polling loop. Use the `pollIntervalMs` the create call returned. |
| `BAD_REQUEST`, `VALIDATION_FAILED`, `CONFLICT`, `REPORT_SCHEMA_UNSUPPORTED`, `UNSAFE_PAYLOAD_REJECTED`      | No. Fix the request.                                                              |
| `UNAUTHENTICATED`, `DEVICE_REVOKED`                                                                         | No. Re-authenticate or re-link.                                                   |
| `FORBIDDEN`, `SUPPORT_GRANT_REQUIRED`                                                                       | No. The principal is not permitted.                                               |
| `NOT_FOUND`, `SHARE_NOT_AVAILABLE`, `REPORT_EXPIRED`, `DEVICE_LINK_EXPIRED`, `DEVICE_LINK_ALREADY_CONSUMED` | No. The resource is gone or was never yours.                                      |

<Note>
  `POST /api/v1/reports` is safe to retry on a network failure: it is idempotent on
  `localReportId` and reuses the same quota reservation rather than charging twice.
</Note>

## What errors never contain

Operational logs and error responses carry ids, codes, and timings — never credentials,
report bodies, source, or raw traffic. Two consequences worth relying on:

* `INTERNAL` gives you a `requestId` and nothing else. The detail is in the server log.
* A share link that is revoked, expired, or simply not permitted for you all answer the same
  `SHARE_NOT_AVAILABLE`, so the response cannot be used to learn whether a report exists.
