# KDPBot API errors

Every non-2xx response from the KDPBot AI API uses one JSON envelope. Branch on `error.code`; never parse `error.message`.

- **API base URL:** `https://bookgen-backend-5zuf.onrender.com/api`
- Related: [quickstart](https://kdpbot.com/docs/quickstart.md) · [authentication](https://kdpbot.com/docs/authentication.md) · [rate limits](https://kdpbot.com/docs/rate-limits.md) · [API reference](https://kdpbot.com/docs/api.md) · [OpenAPI spec](https://kdpbot.com/openapi.json)

---

## The KDPBot API error envelope

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "target_word_count must be a positive integer.",
    "status": 400,
    "details": {
      "target_word_count": ["A valid integer is required."]
    },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

| Field | Type | Always present | Meaning |
| --- | --- | --- | --- |
| `error.code` | string | yes | Stable machine identifier in `SCREAMING_SNAKE_CASE`. **This is the field to branch on.** |
| `error.message` | string | yes | Human-readable prose. Safe to display, not safe to parse — wording changes without notice. |
| `error.status` | integer | yes | Mirrors the HTTP status code, so a single logged object is self-describing. |
| `error.details` | object | no | Structured context. Shape varies by code — field-keyed validation errors, quota counters, allowed formats, and so on. Absent when there is nothing to add. |
| `error.documentation_url` | string | yes | Always `https://kdpbot.com/docs/errors.md`. |

The envelope is the whole response body: there is no sibling key alongside `error`. A successful response never contains an `error` key, so `"error" in body` is a valid success test.

## KDPBot API error codes

| Code | HTTP | When it happens | How to resolve |
| --- | --- | --- | --- |
| `VALIDATION_ERROR` | 400 | Request body failed validation — missing required field, wrong type, value outside an enum, `target_word_count` non-numeric. | Read `error.details`, which is keyed by field name with an array of messages. Correct the payload and resend. **Do not retry unchanged.** Check for a mistyped field name: unknown keys are silently dropped, so a typo shows up as a missing-field error elsewhere. |
| `INVALID_CREDENTIALS` | 401 | `POST /api/auth/login/` with a wrong email/username or password. | Stop. This is a human decision. Do not retry with variations — repeated attempts trigger abuse protection. Offer [password reset](https://kdpbot.com/docs/authentication.md) via `POST /api/auth/forgot-password/`. |
| `AUTHENTICATION_REQUIRED` | 401 | The endpoint requires a token and no `Authorization` header was sent (or the scheme was not `Bearer`). | Attach `Authorization: Bearer <access>` and retry. If you have no token, obtain one from `POST /api/auth/login/`. |
| `TOKEN_EXPIRED` | 401 | The access token is past its 24-hour lifetime. | Call `POST /api/auth/token/refresh/` with your refresh token, store the returned `access` **and** the rotated `refresh`, then replay the original request **once**. |
| `TOKEN_INVALID` | 401 | Token is malformed, has the wrong `token_type`, fails signature verification, or the refresh token itself has expired (30 days). | Refreshing will not help. Discard both tokens and log in again from scratch. |
| `PLAN_UPGRADE_REQUIRED` | 402 | The action is not included in the caller's plan: DOCX/EPUB export on a plan limited to PDF, chapter image generation on the Free plan, or `target_word_count` above `max_word_count_per_book`. | Inspect `error.details` — it carries `allowed_formats`, or `max_allowed` / `requested` for word-count limits. Either reduce the request to fit the plan or direct the user to [https://kdpbot.com/pricing](https://kdpbot.com/pricing). Never retry unchanged; the outcome will not differ. |
| `PERMISSION_DENIED` | 403 | Authenticated, but this account may not perform the action — an admin-only endpoint, or a blocked account. | Stop and escalate to a human. There is no client-side fix and no retry that succeeds. |
| `NOT_FOUND` | 404 | The path does not exist, or the object id does not exist **for this user**. | First check the path: trailing slashes are mandatory, and `export_pdf` / `export_docx` / `export_epub` / `preview_pdf` use underscores while every other action uses hyphens. If the path is right, you are probably authenticated as a different account — cross-user object ids return 404, not 403. |
| `METHOD_NOT_ALLOWED` | 405 | Right path, wrong verb — e.g. `GET` on `/api/books/{id}/generate/`, which is `POST`-only. | Consult [the API reference](https://kdpbot.com/docs/api.md) or [openapi.json](https://kdpbot.com/openapi.json) for the correct method and resend. |
| `CONFLICT` | 409 | The request contradicts current state — for example a uniqueness constraint such as an email already in use. | Do not retry the write unchanged. Read current state first, reconcile, then act. Note: a generation that is already running returns **400 `GENERATION_IN_PROGRESS`**, not 409 — poll `GET /api/v1/books/{id}/generation-status/` instead of retrying. |
| `QUOTA_EXCEEDED` | 429 | A plan allowance is used up for the current billing period — books per month, AI images, AI edits, proofreading checks. | `error.details` carries `quota_type`, `limit`, `used`, `remaining`. This does **not** clear by waiting within a session; the period resets monthly. Either buy [credits](https://kdpbot.com/credits), upgrade at [pricing](https://kdpbot.com/pricing), or stop and tell the user. Check `GET /api/auth/subscriptions/book-creation-options/` before starting work to avoid hitting this mid-flow. |
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests in the current window. | Read the `Retry-After` header and sleep for exactly that many seconds before retrying. See [rate limits](https://kdpbot.com/docs/rate-limits.md). Then reduce your request rate — do not resume at the rate that triggered it. |
| `UPSTREAM_AI_ERROR` | 502 | An upstream model provider failed, timed out, or refused the request (content policy, provider outage, provider rate limit). | Transient in most cases. Retry with exponential backoff, at most 3 attempts. If it persists, the prompt content may have been refused — vary the `description` or chapter `summary`. Generation endpoints are not idempotent: before retrying `POST /api/books/{id}/generate/`, poll `generation-status/` to check whether the first call actually started. |
| `SERVER_ERROR` | 500 | Unhandled server-side failure. | Retry with exponential backoff, at most 3 attempts. If it persists, report it to support@kdpbot.com with the request path, timestamp and (if present) `error.details.request_id`. |

## Worked examples

### 400 — validation

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json
```

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body failed validation.",
    "status": 400,
    "details": {
      "title": ["This field is required."],
      "trim_size": ["\"7x11\" is not a valid choice."]
    },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

### 402 — plan gating on export

```http
HTTP/1.1 402 Payment Required
Content-Type: application/json
```

```json
{
  "error": {
    "code": "PLAN_UPGRADE_REQUIRED",
    "message": "EPUB export is not available on the Free plan.",
    "status": 402,
    "details": {
      "requested_format": "epub",
      "allowed_formats": ["pdf"],
      "upgrade_url": "https://kdpbot.com/pricing"
    },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

### 402 — word count above the plan limit

```json
{
  "error": {
    "code": "PLAN_UPGRADE_REQUIRED",
    "message": "Requested word count exceeds the limit for your plan.",
    "status": 402,
    "details": {
      "requested": 120000,
      "max_allowed": 40000,
      "upgrade_url": "https://kdpbot.com/pricing"
    },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

### 429 — monthly quota exhausted

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
```

```json
{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "You have used all 3 books included in your plan this period.",
    "status": 429,
    "details": {
      "quota_type": "books",
      "limit": 3,
      "used": 3,
      "remaining": 0,
      "period_end": "2026-10-01",
      "upgrade_url": "https://kdpbot.com/pricing"
    },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

Both `QUOTA_EXCEEDED` and `RATE_LIMIT_EXCEEDED` return HTTP 429 but mean different things and require different handling. `RATE_LIMIT_EXCEEDED` clears in seconds; `QUOTA_EXCEEDED` clears at the end of the billing period. **Always read `error.code`, not just the status.**

### 502 — upstream model failure

```json
{
  "error": {
    "code": "UPSTREAM_AI_ERROR",
    "message": "The text generation provider did not respond in time. Please retry.",
    "status": 502,
    "details": { "stage": "chapter_generation", "chapter_number": 7, "retryable": true },
    "documentation_url": "https://kdpbot.com/docs/errors.md"
  }
}
```

## Handling policy for KDPBot API clients

```
code                     retry?          action
──────────────────────────────────────────────────────────────────────
VALIDATION_ERROR         no              fix the payload from error.details
METHOD_NOT_ALLOWED       no              fix the HTTP verb
NOT_FOUND                no              fix the path, or check the account
INVALID_CREDENTIALS      no              escalate to a human
PERMISSION_DENIED        no              escalate to a human
PLAN_UPGRADE_REQUIRED    no              reduce the request, or escalate to upgrade
QUOTA_EXCEEDED           no (in-session) escalate: quota resets monthly
CONFLICT                 no              read current state, then reconcile
AUTHENTICATION_REQUIRED  once            attach the token, replay once
TOKEN_EXPIRED            once            refresh, replay once
TOKEN_INVALID            no              full re-login
RATE_LIMIT_EXCEEDED      yes             sleep Retry-After seconds exactly
UPSTREAM_AI_ERROR        yes, ≤3         exponential backoff; check job state first
SERVER_ERROR             yes, ≤3         exponential backoff
```

Two rules that matter more than the rest:

1. **Never retry a 4xx unchanged** except `TOKEN_EXPIRED`, `AUTHENTICATION_REQUIRED` and `RATE_LIMIT_EXCEEDED`. A 4xx describes something wrong with your request; repeating it produces the same result and burns rate-limit budget.
2. **Check job state before retrying a generation start.** `POST /api/books/{id}/generate/` is not idempotent and consumes quota on completion. If it appears to fail, call `GET /api/books/{id}/generation-status/` — a `running` status means the first call succeeded and a retry would be a duplicate.

## Reporting a problem

Include the request method and full path, the UTC timestamp, the `error.code`, and `error.details.request_id` if present. Do **not** include your access or refresh token.

support@kdpbot.com · [Telegram](https://t.me/blabla_ecommerce) · [WhatsApp](https://wa.me/380994019521) · [https://kdpbot.com/contacts](https://kdpbot.com/contacts)

## More KDPBot AI documentation

- [Quickstart](https://kdpbot.com/docs/quickstart.md) · [Authentication](https://kdpbot.com/docs/authentication.md) · [Rate limits](https://kdpbot.com/docs/rate-limits.md) · [API reference](https://kdpbot.com/docs/api.md)
- [MCP and agent integration](https://kdpbot.com/docs/mcp.md) · [Agent instructions](https://kdpbot.com/agents.md) · [llms.txt](https://kdpbot.com/llms.txt)
- [KDPBot OpenAPI specification (JSON)](https://kdpbot.com/openapi.json) · [OpenAPI 3.1 (YAML)](https://kdpbot.com/openapi.yaml) · [KDPBot developer portal](https://kdpbot.com/developers/) · [API catalog](https://kdpbot.com/.well-known/api-catalog)
