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

# Errors

> Every error is a stable JSON envelope with a machine-readable code.

All errors - including unexpected server errors - return:

```json theme={null}
{
  "error": "Human-readable message",
  "code": "MACHINE_READABLE_CODE"
}
```

Some errors add context fields (`details`, `limit`, `tokenBalance`, `resetAt`, ...). The `code` values are stable; branch on them, not on messages.

<Warning>
  Branch on `code`, never on `error`. The `error` string is prose and it changes: on `402
      INSUFFICIENT_TOKENS` and `403 STARTER_LIMIT_REACHED` for accounts on the free plan it now runs to
  a sentence or two and includes a URL. `code` and the HTTP status did not move and will not.
</Warning>

## The `upsell` object

`402 INSUFFICIENT_TOKENS` and `403 STARTER_LIMIT_REACHED` may carry an extra `upsell` object.
Free accounts receive the `founder_report` offer; a paid, non-regional account with no deep
credits may receive a `scan_pack` top-up on `402`. It is additive and optional, absent on batch
items, and may be absent at any time, so treat its presence as a bonus, never as a contract:

```json theme={null}
{
  "error": "No deep credits on this account. The Founder Report unlocks ...",
  "code": "INSUFFICIENT_TOKENS",
  "tokenBalance": 0,
  "upsell": {
    "offer": "founder_report",
    "price": "$59 one-time",
    "guarantee": "14-day satisfaction guarantee",
    "url": "https://preuve.ai/pricing?src=agent_api&utm_source=agent_api&utm_medium=agent",
    "locked": ["Market size (TAM/SAM/SOM)", "Up to 15 competitors with funding and weak spots"],
    "note": "One relay-ready sentence saying the same thing."
  }
}
```

The same object also appears on `GET /api/agent/analyses/:id` when a **starter** scan owned by
a free account has finished. `note` is written to be relayed to a human as-is.

`guarantee` appears only on `offer: "founder_report"`. It is omitted from `scan_pack` top-ups.

`url` is tagged with the door the request came through: `utm_source=agent_api` for direct
`x-preuve-key` calls like the one above, `utm_source=mcp` through the hosted MCP server.

**An error carrying `upsell` is a quota or billing answer, never a bad key.** If you are an
agent, do not send the user to the API-keys panel for one.

## Common codes

| HTTP | Code | Meaning | What to do |
| - | - | - | - |
| 400 | `INVALID_MODULES` | Unknown or malformed `modules` array | Fix the module names |
| 400 | `INVALID_FOUNDER_PROFILE` | Missing/invalid `founderProfile` for `founderFit` | Fix the fields listed in `details` |
| 400 | `INVALID_ANALYSIS_ID` | Not a UUID (a title, a share slug, a URL) | Use the run id from create or the report id (the UUID in the report URL) |
| 401 | (auth codes) | Missing, malformed, unknown or revoked key | Check [Authentication](/authentication) |
| 402 | `INSUFFICIENT_TOKENS` | Deep scan with no quota and no tokens | Top up or use `scanType: "starter"` |
| 403 | `MODULES_REQUIRE_DEEP` | Modules requested on a non-deep report | Run a deep scan first |
| 403 | `STARTER_LIMIT_REACHED` | Starter scan allowance exhausted (free/regional) | Wait for reset or run deep |
| 429 | `FAIR_USE_LIMIT` | Paid account hit the starter fair-use ceiling | Contact support to lift it |
| 404 | `ANALYSIS_NOT_FOUND` | Unknown id, or the run/report belongs to another account. On the poll only, any personal starter or deep report the account owns resolves, dashboard-created ones included; export and enrich answer 403 instead. Agency client reports need `workspace: "agency"` | Check the id |
| 403 | `REPORT_NOT_FROM_AGENT_RUN` | Export/enrich of a report created on the dashboard | Poll it with get\_analysis; for its data relay the refusal text, which names the way forward for that report, or start a new analysis here |
| 404 | `REPORT_DELETED` | The run's report was deleted from the dashboard | Nothing to poll, enrich or export; create a new run |
| 404 | `NOT_FOUND` | No route matches the method and path | Read `details.openapi` or `GET /api/agent` |
| 409 | `ANALYSIS_NOT_COMPLETE` | Run still processing | Keep polling |
| 409 | `ENRICHMENT_NOT_COMPLETE` | Export requested before enrichment | Call enrich, poll, retry |
| 409 | `MODULE_ALREADY_GENERATED` | Per-module cap reached (one success per report) | Read the payload from the export |
| 413 | `PAYLOAD_TOO_LARGE` | Body over the size limit | Trim the request |
| 422 | `ANALYSIS_FAILED` | The analysis itself failed | Create a new run |
| 429 | `RATE_LIMITED` | Per-route rate limit hit | Back off and retry |
| 429 | `CONCURRENT_LIMIT_REACHED` | Too many analyses in flight (3 free / 2 deep) | Wait for one to finish, re-POST the same `clientRunId` |
| 429 | `DAILY_LIMIT_REACHED` | Rolling 24h ceiling per account, on run creation or paid idea generation (20, of which 15 Pro) | Retry after `resetAt` (frees one slot). Idea generation sends only `limit`, and each slot frees 24h after its generation |
| 429 | `REGEN_LIMIT_REACHED` | Founder Fit lifetime cap on this report | No retry - cap is final |
| 503 | `INSUFFICIENT_TIME_BUDGET` | Inline module refused to start late in the request | Retry the same enrich call |
| 503 | `SERVICE_DISABLED` / `GENERATION_DISABLED` / `POD_DISABLED` | Feature temporarily off | Retry later |
| 502 | `ENQUEUE_FAILED` | Background dispatch failed | Retry; nothing was consumed |
| 500 | `INTERNAL_ERROR` | Unexpected server error | Retry with backoff |

## Retry-After

A `429 RATE_LIMITED` carries a `Retry-After` response header: whole seconds
until the limiter's window rolls over. Those windows are fixed one-minute
buckets, so the number is exact, not an estimate - waiting that long is enough,
and retrying sooner is not.

```
HTTP/1.1 429 Too Many Requests
Retry-After: 37
```

The JSON body is unchanged; keep branching on `code`. The quota-shaped 429s
(`CONCURRENT_LIMIT_REACHED`, `DAILY_LIMIT_REACHED`, `FAIR_USE_LIMIT`,
`REGEN_LIMIT_REACHED`) send no header - they clear on a different clock, and
`resetAt` in the body is the field to use where one applies.

## Retired codes

These were part of the HMAC request-signing scheme and can no longer be returned
as of **2026-07-29**. They are listed so a client that still branches on them
knows the branch is dead, not that the docs forgot a code.

| Was | Code | Why it is gone |
| - | - | - |
| 401 | `MISSING_SIGNATURE_HEADERS` | One header now, so the failure is `MISSING_API_KEY` |
| 401 | `INVALID_SIGNATURE` / `TIMESTAMP_SKEW` / `INVALID_TIMESTAMP` | Nothing is signed or timestamped |
| 409 | `NONCE_REPLAY` | No nonce, so an identical request is simply replayable |

An identical request sent twice now succeeds twice. If you relied on
`409 NONCE_REPLAY` for idempotency, use `clientRunId` instead - that is the real
idempotency key and always was.

## Partial success (207)

Enrich returns HTTP `207` with a **success-shaped** body when some sections or modules failed while others succeeded. Inspect `failures` (core sections) and the per-module `modules` map, then retry the failed parts - the route is idempotent and never regenerates what already completed.


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