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

# Error Handling Catalog

> How to classify M2M API failures, decide when to retry, and expose safe errors in production integrations.

Use this page to standardize how your M2M client handles non-2xx responses, network failures, polling timeouts, and user-facing fallbacks.

The API Reference shows endpoint-level response schemas. This page focuses on client behavior.

## Error response model

For failed requests, treat the HTTP status code as the primary contract.

The response body may include an `error` field or a small JSON object with details:

```json theme={null}
{
  "error": "invalid payload"
}
```

Do not build production logic around exact error text. Exact wording can change. Use:

* HTTP status
* endpoint name
* request context you own
* retry count
* latency
* `request_id` when the response includes one

## Status code handling

| Status | Meaning                                                                 | Client behavior                                                                    |
| ------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `400`  | Invalid request payload, missing field, or malformed input              | Fix request construction. Do not retry without changing the request.               |
| `401`  | Missing or invalid authentication                                       | Treat as a configuration or credential incident. Do not retry repeatedly.          |
| `403`  | Valid identity but action is not allowed                                | Check plan, tenant access, site access, or allowed capability. Do not blind-retry. |
| `404`  | Tenant, site, request, or document was not found in the requested scope | Check identifiers and ownership. Do not blind-retry.                               |
| `409`  | Conflict, when returned by an endpoint                                  | Check whether the resource already exists or whether the operation is duplicated.  |
| `429`  | Too many requests                                                       | Retry with bounded exponential backoff and jitter.                                 |
| `5xx`  | Temporary server-side or upstream failure                               | Retry with bounded exponential backoff for safely repeatable operations.           |

## Retry decision table

| Operation                       | Retry on `429` | Retry on `5xx` | Retry on timeout       | Notes                                                               |
| ------------------------------- | -------------- | -------------- | ---------------------- | ------------------------------------------------------------------- |
| Read tenant or site information | Yes            | Yes            | Yes                    | Safe to retry with backoff.                                         |
| Read sessions or statistics     | Yes            | Yes            | Yes                    | Safe to retry with backoff.                                         |
| Create text or Q\&A document    | Yes, bounded   | Yes, bounded   | Use care               | Avoid creating duplicates in your own sync process.                 |
| Upload file document            | Yes, bounded   | Yes, bounded   | Use care               | Keep a source-to-document sync log in your system.                  |
| Sync chat                       | Yes, bounded   | Yes, bounded   | Use care               | The user may have already waited; show a fallback on final failure. |
| Async chat submit               | Yes, bounded   | Yes, bounded   | Use care               | Store local job state and avoid duplicate user notifications.       |
| Async chat polling              | Yes            | Yes            | Stop at timeout budget | Polling should never run forever.                                   |
| Feedback submission             | Yes, bounded   | Yes, bounded   | Use care               | Avoid duplicate feedback events from the same UI event.             |
| Session close                   | Yes, bounded   | Yes, bounded   | Yes                    | Safe if your app treats close as idempotent at the workflow level.  |

## JavaScript error class

Use a custom error type so application code can make decisions without parsing strings.

```js theme={null}
export class UppzyApiError extends Error {
  constructor(message, { status, endpoint, details, retryable }) {
    super(message);
    this.name = "UppzyApiError";
    this.status = status;
    this.endpoint = endpoint;
    this.details = details;
    this.retryable = retryable;
  }
}

export function isRetryableStatus(status) {
  return status === 429 || (status >= 500 && status <= 599);
}
```

## JavaScript client with timeout and retry

This example retries only retryable failures and applies a total timeout per request attempt.

```js theme={null}
import { UppzyApiError, isRetryableStatus } from "./uppzy-errors.js";

const BASE_URL = process.env.UPPZY_BASE_URL || "https://api.uppzy.com/api/v1";
const API_KEY = process.env.UPPZY_API_KEY;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function backoffDelay(attempt) {
  const baseMs = 300;
  const maxMs = 4000;
  const jitterMs = Math.floor(Math.random() * 250);
  return Math.min(maxMs, baseMs * 2 ** attempt) + jitterMs;
}

export async function uppzyRequest(path, {
  method = "GET",
  body,
  timeoutMs = 15000,
  maxRetries = 2,
} = {}) {
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const response = await fetch(`${BASE_URL}${path}`, {
        method,
        signal: controller.signal,
        headers: {
          "X-API-Key": API_KEY,
          "Content-Type": "application/json",
        },
        body: body ? JSON.stringify(body) : undefined,
      });

      const text = await response.text();
      const data = text ? JSON.parse(text) : null;

      if (response.ok) {
        return data;
      }

      const retryable = isRetryableStatus(response.status);

      if (!retryable || attempt === maxRetries) {
        throw new UppzyApiError(`Uppzy API returned HTTP ${response.status}`, {
          status: response.status,
          endpoint: path,
          details: data,
          retryable,
        });
      }

      await sleep(backoffDelay(attempt));
    } catch (error) {
      if (error instanceof UppzyApiError) {
        throw error;
      }

      const isAbort = error.name === "AbortError";
      const canRetry = isAbort && attempt < maxRetries;

      if (!canRetry) {
        throw new UppzyApiError(isAbort ? "Uppzy API request timed out" : "Uppzy API request failed", {
          status: null,
          endpoint: path,
          details: null,
          retryable: isAbort,
        });
      }

      await sleep(backoffDelay(attempt));
    } finally {
      clearTimeout(timeout);
    }
  }
}
```

## Safe user-facing fallback

Do not expose raw API errors to end users. Map integration failures to a small set of safe messages in your product.

```js theme={null}
export function userFacingAssistantError(error) {
  if (error.status === 401 || error.status === 403) {
    return "The assistant is temporarily unavailable.";
  }

  if (error.status === 404) {
    return "The assistant could not find the requested support context.";
  }

  if (error.status === 429 || error.retryable) {
    return "The assistant is taking longer than expected. Please try again shortly.";
  }

  return "The assistant could not complete the request.";
}
```

Keep detailed error information in server-side logs only.

## Async polling failure handling

For async chat, distinguish submit failures from polling failures.

```js theme={null}
import { uppzyRequest } from "./uppzy-request.js";

const SITE_ID = process.env.UPPZY_SITE_ID;

export async function waitForAsyncAnswer(requestId, {
  intervalMs = 2500,
  timeoutMs = 45000,
} = {}) {
  const startedAt = Date.now();

  while (Date.now() - startedAt < timeoutMs) {
    const result = await uppzyRequest(`/m2m/sites/${SITE_ID}/chat/requests/${requestId}`, {
      method: "GET",
      maxRetries: 1,
    });

    if (result.status === "completed") {
      return result;
    }

    if (result.status === "failed") {
      throw new Error(result.error || "Async chat failed");
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error("Async chat polling timed out");
}
```

Recommended behavior after polling timeout:

* Mark the local job as timed out
* Store the `request_id`
* Avoid endless polling
* Let the user retry through your own product flow
* Review timeout frequency in monitoring

## Logging checklist

Log enough to debug without leaking sensitive data.

Recommended fields:

* Integration name
* Environment
* Endpoint path template
* HTTP status
* Tenant ID and site ID
* Session ID
* Request ID when available
* Retry count
* Latency
* Timeout flag

Do not log:

* API keys
* Authorization headers
* Full secret configuration
* Raw private customer records
* Full document content in normal production logs

## Incident routing

Use these routing rules when alerts fire:

* Repeated `401`: check key rotation, deployment configuration, and revoked keys
* Repeated `403`: check plan level, tenant access, site access, and enabled capability
* Repeated `404`: check configured `tenant_id`, `site_id`, `request_id`, or document ID
* Repeated `429`: reduce concurrency, add backoff, and inspect traffic spikes
* Repeated `5xx`: keep bounded retries, monitor recovery, and pause non-critical bulk jobs if needed
* Polling timeouts: check async job volume, user timeout budget, and worker concurrency

## Production checklist

* All HTTP calls have timeout settings
* Retries are bounded
* Retryable and non-retryable failures are separated
* `401` and `403` create operational alerts instead of retry storms
* Async polling has an overall timeout
* User-facing errors are safe and non-technical
* Server logs exclude API keys and sensitive customer records
* Bulk document sync jobs can pause when error rate increases
