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

# Error handling

> Understand error response formats and handle API errors in your application

When a request fails, the API returns a JSON response with `success: false` and an `error` object containing a machine-readable code, a human-readable message, and the HTTP status code.

## Error response format

```json theme={null}
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "status": 404
  }
}
```

<ResponseField name="success" type="boolean">
  Always `false` for error responses.
</ResponseField>

<ResponseField name="error" type="object">
  <ResponseField name="code" type="string">
    A machine-readable error code. Use this for programmatic error handling.
  </ResponseField>

  <ResponseField name="message" type="string">
    A human-readable description of the error. Suitable for logging but not guaranteed to be stable across versions.
  </ResponseField>

  <ResponseField name="status" type="number">
    The HTTP status code associated with the error.
  </ResponseField>
</ResponseField>

## Error codes

| Code                  | HTTP status | Description                                                                                                           |
| --------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHORIZED`        | 401         | No authentication header provided. Include `Authorization: Bearer hg_live_YOUR_KEY` or `X-API-Key: hg_live_YOUR_KEY`. |
| `INVALID_KEY_PREFIX`  | 401         | API key format is invalid. Keys must start with `hg_live_` or `hg_admin_`.                                            |
| `KEY_NOT_FOUND`       | 401         | API key not found or has been revoked. Generate a new key at Settings → API Keys.                                     |
| `INVALID_TOKEN`       | 401         | Session token is malformed, expired, or has been revoked. Refresh the session and retry.                              |
| `FORBIDDEN`           | 403         | The authenticated user does not have permission to access this resource or perform this action.                       |
| `NOT_FOUND`           | 404         | The requested resource does not exist or the user does not have access to it.                                         |
| `VALIDATION_ERROR`    | 400         | The request body is missing required fields, contains invalid data, or has malformed JSON.                            |
| `AGENT_NOT_FOUND`     | 404         | The specified genie (agent) does not exist or the user does not have access to it.                                    |
| `INVALID_ACTION`      | 400         | The `action` value is not supported for the specified resource.                                                       |
| `RATE_LIMIT_EXCEEDED` | 429         | Too many requests. API keys are limited to 60 requests/minute. Wait before retrying.                                  |
| `INTERNAL_ERROR`      | 500         | An unexpected server error occurred. If this persists, contact support.                                               |

## Handling errors in code

### With ApiService

`ApiService.invoke()` throws on error. Wrap calls in try/catch:

```typescript theme={null}
import { ApiService } from "@/services/api/ApiService";

try {
  const genie = await ApiService.invoke({
    resource: "genies",
    action: "get",
    id: "abc-123",
  });
} catch (error) {
  console.error("API request failed:", error.message);
}
```

### With fetch

When using `fetch` directly, check the `success` field in the response body:

```typescript theme={null}
const response = await fetch(
  "https://api.helpgenie.ai/v1",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer hg_live_YOUR_KEY`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      resource: "genies",
      action: "get",
      id: "abc-123",
    }),
  }
);

const result = await response.json();

if (!result.success) {
  const { code, message, status } = result.error;

  switch (code) {
    case "UNAUTHORIZED":
    case "INVALID_TOKEN":
      // Refresh the auth session and retry
      break;
    case "NOT_FOUND":
    case "AGENT_NOT_FOUND":
      // Resource does not exist or is not accessible
      break;
    case "VALIDATION_ERROR":
      // Fix the request body and retry
      break;
    case "RATE_LIMIT_EXCEEDED":
      // Wait and retry with exponential backoff
      break;
    default:
      console.error(`API error [${code}]: ${message}`);
  }
}
```

### With TanStack Query

When using TanStack Query (React Query), errors propagate through the query's `error` state:

```typescript theme={null}
import { useQuery } from "@tanstack/react-query";
import { ApiService } from "@/services/api/ApiService";

const { data, error, isError } = useQuery({
  queryKey: ["genies"],
  queryFn: () =>
    ApiService.invoke({
      resource: "genies",
      action: "all",
    }),
});

if (isError) {
  console.error("Failed to load genies:", error.message);
}
```

## Retry strategy

<Tip>
  **Use exponential backoff for `429` and `500` errors.** These are the only error codes worth retrying automatically. Authentication errors (`401`) require a token refresh, and validation errors (`400`, `403`, `404`) indicate a problem with the request itself.

  A recommended approach:

  1. Start with a **1-second** delay after the first failure.
  2. **Double** the delay after each subsequent failure (1s, 2s, 4s, 8s...).
  3. Add **random jitter** (0-500ms) to avoid thundering herd problems.
  4. Give up after **3-4 retries** and surface the error to the user.
</Tip>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const delay = (ms: number) =>
    new Promise((resolve) => setTimeout(resolve, ms));

  async function fetchWithRetry<T>(
    fn: () => Promise<T>,
    maxRetries = 3
  ): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error: any) {
        const code = error?.code ?? error?.error?.code;
        const isRetryable =
          code === "RATE_LIMIT_EXCEEDED" || code === "INTERNAL_ERROR";

        if (!isRetryable || attempt === maxRetries) {
          throw error;
        }

        const backoff = 1000 * Math.pow(2, attempt);
        const jitter = Math.random() * 500;
        await delay(backoff + jitter);
      }
    }
    throw new Error("Max retries exceeded");
  }

  // Usage
  const genies = await fetchWithRetry(() =>
    ApiService.invoke({ resource: "genies", action: "all" })
  );
  ```

  ```bash cURL (with retry loop) theme={null}
  #!/bin/bash
  # Retry with exponential backoff for 429 and 500 errors

  URL="https://api.helpgenie.ai/v1"
  TOKEN="hg_live_YOUR_KEY"
  MAX_RETRIES=3

  for attempt in $(seq 0 $MAX_RETRIES); do
    RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$URL" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"resource": "genies", "action": "all"}')

    HTTP_CODE=$(echo "$RESPONSE" | tail -1)
    BODY=$(echo "$RESPONSE" | sed '$d')

    if [ "$HTTP_CODE" -ne 429 ] && [ "$HTTP_CODE" -ne 500 ]; then
      echo "$BODY"
      exit 0
    fi

    if [ "$attempt" -eq "$MAX_RETRIES" ]; then
      echo "Max retries exceeded. Last response: $BODY" >&2
      exit 1
    fi

    DELAY=$(( (1 << attempt) ))
    echo "Retrying in ${DELAY}s (attempt $((attempt + 1))/$MAX_RETRIES)..." >&2
    sleep "$DELAY"
  done
  ```
</CodeGroup>

## Common error scenarios

<AccordionGroup>
  <Accordion title="401 — Missing or expired token">
    The most common cause is an expired access token. Supabase tokens typically expire after 1 hour. Use `supabase.auth.refreshSession()` to get a new token.

    ```typescript theme={null}
    const { data, error } = await supabase.auth.refreshSession();
    const newToken = data.session?.access_token;
    ```
  </Accordion>

  <Accordion title="400 — Invalid action for resource">
    Each resource supports a specific set of actions. Sending an unsupported action (e.g., `action: "archive"` to a resource that does not support it) returns `INVALID_ACTION`. Check the resource's documentation in the [API reference](/api-reference/introduction) for supported actions.
  </Accordion>

  <Accordion title="403 — Insufficient permissions">
    The user's role does not allow the requested operation. For example, a `consumer` user cannot create genies. See [Introduction](/getting-started/introduction) for the role permission matrix.
  </Accordion>

  <Accordion title="404 — Resource not found">
    The record either does not exist or belongs to another user. Non-admin users can only access their own resources. Verify the `id` value and confirm the user has access.
  </Accordion>

  <Accordion title="429 — Rate limit exceeded">
    Back off and retry with exponential delay. A simple implementation:

    ```typescript theme={null}
    const delay = (ms: number) =>
      new Promise((resolve) => setTimeout(resolve, ms));

    async function retryWithBackoff<T>(
      fn: () => Promise<T>,
      maxRetries = 3
    ): Promise<T> {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
          return await fn();
        } catch (error: any) {
          if (
            error?.code === "RATE_LIMIT_EXCEEDED" &&
            attempt < maxRetries - 1
          ) {
            await delay(1000 * Math.pow(2, attempt));
            continue;
          }
          throw error;
        }
      }
      throw new Error("Max retries exceeded");
    }
    ```
  </Accordion>
</AccordionGroup>
