> ## 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 name="details" type="object">
    Optional structured detail. For example, a request sent with `"strict": true` that includes undocumented fields returns `unknown_fields` and `allowed_fields`. See [Undocumented fields and strict mode](/api-reference/making-requests#undocumented-fields-and-strict-mode).
  </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 [API reference](/api-reference/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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.