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

# Errors

> Unified error catalog. Every code returned by the Dacard.ai API, with HTTP status, retryability, and recommended action.

The API returns errors in a single shape. Every response with a non-2xx status carries a JSON body with `code`, `message`, `action`, and `retryable`. Match the `code` field, not the message string. Messages are written for end users and may change without notice.

## Response shape

```json theme={null}
{
  "error": {
    "code": "PLAN_LIMIT_REACHED",
    "message": "You're at the top of your plan.",
    "action": "Move up a plan for more reads.",
    "retryable": false
  }
}
```

Some legacy routes return `{ "error": "string" }` only. Treat the HTTP status as authoritative when no `code` is present.

## Retry posture

| Retryable | Meaning |
| - | - |
| `true` | The condition is transient. Back off, then retry. Use exponential backoff for 5xx and rate limit errors. |
| `false` | The condition is permanent for this caller, this resource, or this plan. Retrying without changing inputs will fail again. |

For `429` rate limit errors, see [Rate Limits](https://docs.dacard.ai/api-reference/rate-limits.md).

## Code catalog

| Code | HTTP | Retryable | What it means | What to do |
| - | - | - | - | - |
| `AUTH_REQUIRED` | 401 | No | The request has no valid Clerk session or API key. | Sign in, mint an API key, or attach `Authorization: Bearer <token>` and retry. |
| `FORBIDDEN` | 403 | No | The caller is authenticated but does not own the resource or lacks the required role. | Use an account that has access. Check RBAC scope on `/api-reference/api-keys`. |
| `ONBOARDING_REQUIRED` | 403 | No | The user has not completed the activation contract (Day 1 interview + Week 1 org map). | Send the user through `/onboarding/welcome`. |
| `PLAN_LIMIT_REACHED` | 402 | No | The account has hit a plan ceiling (products, members, or feature gate). | Move up a plan. See `/api-reference/billing/create-checkout-session`. |
| `CREDIT_EXHAUSTED` | 402 | No | The monthly credit pool is empty for this billing cycle. | Move up a plan, or wait for the next reset. Check `/api-reference/user/get-usage-and-quota`. |
| `FEATURE_GATED` | 403 | No | The feature exists on a higher tier than the caller's current plan. | Move up a plan to unlock. |
| `SCORING_FAILED` | 500 | Yes | The scoring engine could not produce a result. The site may be too large or scripted. | Retry with a simpler URL, or point the read at a different page. |
| `SCORING_TIMEOUT` | 504 | Yes | The scoring run exceeded the time budget. | Retry. If it persists, point the read at a leaner page. |
| `SCORING_RATE_LIMITED` | 429 | Yes | More than 5 score calls in the trailing 60-second window. | Back off 60 seconds, then retry. See [Rate Limits](https://docs.dacard.ai/api-reference/rate-limits.md). |
| `CRAWL_FAILED` | 502 | Yes | The public-web crawl could not reach the URL. | Confirm the URL is public and reachable. Retry. |
| `CRAWL_TIMEOUT` | 504 | Yes | The crawler hit the per-page time budget. | Retry, or point at a faster page. |
| `EMPTY_CRAWL` | 422 | Yes | The crawler reached the URL but found no scoreable content. | Point at the main product page, not a marketing redirect. |
| `INTEGRATION_DISCONNECT` | 401 | No | The integration's OAuth token expired or was revoked. | Reconnect the integration in `Settings > Integrations`. |
| `TEAM_INVITE_EXPIRED` | 410 | No | The invite token has aged out. | Ask an admin to resend the invite. |
| `SIGNAL_VALIDATION_FAILED` | 400 | Yes | A signal-card submission missed required fields. | Complete the assessment, then retry. |
| `SIGNAL_RATE_LIMITED` | 429 | Yes | A signal card was generated for the same target within the last hour. | Wait an hour, then retry. |
| `READ_ONLY_MODE_ACTIVE` | 403 | No | The Free account hit the day-14 activation cliff with one or both onboarding milestones still incomplete. Write actions are blocked until rescue. | Finish the day-1 interview AND the week-1 org map (rescue path), or move up to Pro. See [Path C+ activation contract](https://docs.dacard.ai/knowledge-base/getting-started.md). |
| `NETWORK_ERROR` | 502 | Yes | An upstream dependency was unreachable from the API. | Retry with backoff. |
| `INTERNAL_ERROR` | 500 | Yes | Unhandled exception inside the API. The trace was sent to Sentry. | Retry once. If it persists, ping support with the request ID. |

## Where each code shows up

Most codes are not endpoint-specific. The list below covers the ones a developer can predict from the call site.

* **`AUTH_REQUIRED` / `FORBIDDEN`**, every authenticated route returns these. Check before calling anything other than `/api/health` or `/api/score/anonymous`.
* **`PLAN_LIMIT_REACHED` / `CREDIT_EXHAUSTED`**, `/api/score`, `/api/chat`, `/api/agents/run`, `/api/teams` (POST).
* **`FEATURE_GATED`**, Business-and-up routes: `/api/teams`, `/api/settings/byom`, `/api/intelligence/portfolio`.
* **`ONBOARDING_REQUIRED`**, `/api/quarter-close`, `/api/board-report`, `/api/briefing`.
* **`SCORING_*` / `CRAWL_*` / `EMPTY_CRAWL`**, `/api/score`, `/api/score/quick`, `/api/score/product`.
* **`INTEGRATION_DISCONNECT`**, `/api/integrations/sync`, any `/api/integrations/*` action route.
* **`SIGNAL_*`**, `/api/signal-card/[id]`, `/api/signal-card/[id]/link`.
* **`TEAM_INVITE_EXPIRED`**, `/api/teams/[id]/accept`.
* **`READ_ONLY_MODE_ACTIVE`**, every write route on Free accounts past day 14 with incomplete activation: `/api/score`, `/api/score/product`, `/api/chat`, `/api/products` (POST), `/api/teams` (POST). Cleared on rescue (both milestones complete) or on Pro upgrade.

## Handling errors in code

```ts theme={null}
import type { ErrorCode } from '@/lib/errors';

interface ApiError {
  code: ErrorCode;
  message: string;
  action?: string;
  retryable: boolean;
}

async function callDacard<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`https://app.dacard.ai${path}`, init);
  if (!res.ok) {
    const body = (await res.json().catch(() => ({}))) as { error?: ApiError };
    const err = body.error;

    if (err?.code === 'CREDIT_EXHAUSTED' || err?.code === 'PLAN_LIMIT_REACHED') {
      // Stop. Surface the plan upgrade flow.
      throw new Error(err.message);
    }

    if (err?.retryable && res.status >= 500) {
      // Retry with exponential backoff (handled by the caller).
      throw Object.assign(new Error(err.message), { retryable: true });
    }

    throw new Error(err?.message ?? `HTTP ${res.status}`);
  }
  return res.json() as Promise<T>;
}
```

## Source of truth

The full code list lives in `apps/web/src/lib/errors.ts`. New codes added to that file ship to this page on the next docs build.


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