# BeYourCover Partner API

Enterprise/partner REST API for AI-powered book cover generation. All endpoints require a partner API key (`Authorization: Bearer byc_sk_...`). Long-running operations (generation, variations, edits, upscales, audiobook covers) return `202 Accepted` with a job envelope; poll `GET /api/v1/jobs/{jobId}` or receive a webhook. All error responses use a uniform envelope: `{ "error": { "code": string, "message": string } }`.

Base URL: `https://beyourcover.com`

Machine-readable version of this reference: [/openapi.json](/openapi.json). Human version: [/api-docs](/api-docs).

## Authentication

Partner API key sent as `Authorization: Bearer byc_sk_...`. The key must match `^byc_sk_[0-9a-f]{32}$`. A malformed, unknown, or inactive key yields `401 unauthorized`; a valid key whose owner has no active enterprise subscription yields `403 forbidden`.

## MCP server (for AI agents)

The same pipeline is exposed as a remote MCP server at `https://beyourcover.com/api/mcp/mcp` (Streamable HTTP transport only; no SSE endpoint), authenticated with the same Bearer API key. Tools: `create_book_and_generate_covers`, `generate_covers_for_book`, `get_generation_status`, `list_templates_for_genre`, `get_credit_balance`, `list_books`, `list_covers_for_book` (free, no credits), `download_cover`, `upscale_cover` (free, no credits), `edit_cover`, `remove_cover_text`, `upload_reference_image` (free, no credits), `generate_cover_from_reference`, `create_audiobook_cover`, `get_wrap` (free, no credits), `create_full_wrap` (full print wrap: back artwork, typeset back cover, spine and extended front, returning a KDP-ready PDF), `update_wrap_text` (re-typeset a wrap, free and synchronous), `extend_wrap_front` (re-run just the front extension, free), `calculate_kdp_wrap` (free, no credits). Generation and follow-on operations are async — poll `get_generation_status` for 1-hour signed download URLs; for wrap jobs it returns the wrapId with its preview, flat and PDF plus any layout warnings. Finished covers are additionally returned as inline MCP image content by `get_generation_status` and `download_cover` (pass `include_images`/`include_image` false for URL-only responses; print-resolution assets are always URL-only).

## Endpoints

### POST /api/v1/books

Create a book record

Creates a book that covers can be generated against. `author` is required unless `genre` is the planner genre (`Planner & Journal` / `planner-journal`). When `subgenre` is omitted for a high-variance genre, the server auto-detects one from the book details (an explicit `subgenre` always wins; detection failure leaves it null). Supports idempotent replay via the `Idempotency-Key` header: a repeated key returns `200` with the previously created book instead of `201`.

**Parameters:**

- `undefined` (, string)

**Request body** (JSON):

- `title` (string, required, max 100 chars) — Book title. Required. Rendered on the cover, so Arabic and Hindi/Devanagari scripts are rejected (`unsupported_characters`).
- `author` (string, max 100 chars) — Author name. Required unless `genre` is `Planner & Journal` / `planner-journal`. Same script restrictions as `title`.
- `subtitle` (string, max 300 chars) — Optional subtitle. Same script restrictions as `title`.
- `genre` (string, required) — Required. Accepts either a kebab-case genre slug (e.g. `science-fiction`, `business-economics`, `religion-spirituality`, `non-fiction`, `fantasy`, `romance`, `thriller`, `mystery`, `horror`, `historical-fiction`, `health-fitness`, `contemporary-fiction`, `young-adult`, `childrens`, `self-help`, `biography-memoir`, `poetry`, `planner-journal`, `other`) or the canonical genre value (e.g. `Science Fiction`). Unrecognized values return `invalid_genre`.
- `subgenre` (string, max 100 chars) — Optional subgenre value. If omitted for genres that have subgenres, the server auto-detects one.
- `summary` (string, max 1000 chars) — Optional plot/content summary used to inform generation.
- `target_audience` (string, max 500 chars) — Optional. Must be one of the predefined audience values. Allowed: `children`, `middle-grade`, `young-adult`, `new-adult`, `adult`.
- `external_ref` (string, max 200 chars) — Optional caller-supplied reference. Echoed back in the response (not stored on the book).
- `planner_year` (string) — Optional. Only meaningful for the planner genre. Must be a string if provided (type-checked; no length limit is enforced in code).

**Responses:**

- `200` — Idempotent replay: a book with the same `Idempotency-Key` already exists for this user. `external_ref` is echoed from the current request, not the original one. (`BookCreated`)
- `201` — Book created. (`BookCreated`)
- `400` — Validation or moderation failure. `code` is one of: `missing_required_fields`, `invalid_field_type`, `field_too_long`, `unsupported_characters`, `invalid_field_value`, `content_policy_violation`, `invalid_genre`. (`ErrorEnvelope`) Error codes: `missing_required_fields`, `invalid_field_type`, `field_too_long`, `unsupported_characters`, `invalid_field_value`, `content_policy_violation`, `invalid_genre`.
- `401` — 
- `403` — 
- `500` — Server error. `code` is `book_creation_failed` (insert failed) or `internal_error`. (`ErrorEnvelope`) Error codes: `book_creation_failed`, `internal_error`.

### GET /api/v1/books

List books

Lists books owned by the API key's account, newest first, with keyset cursor pagination.

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Responses:**

- `200` — One page of books. (`BookList`)
- `400` — `code` is one of: `invalid_limit`, `invalid_cursor`. (`ErrorEnvelope`) Error codes: `invalid_limit`, `invalid_cursor`.
- `401` — 
- `403` — 
- `500` — Server error. `code` is `fetch_failed` or `internal_error`. (`ErrorEnvelope`) Error codes: `fetch_failed`, `internal_error`.

### POST /api/v1/covers/generate

Generate covers for a book (async)

Queues an asynchronous cover-generation job through the V2 engine and returns `202` immediately. Costs `count` credits (reserved up-front; unused credits for partially/fully failed generations are refunded). Genres/subgenres not supported by the V2 engine are rejected with `422 unsupported_genre`. Note: unlike the other job-creating endpoints, the fresh `202` envelope from this endpoint does NOT include an `operation` field (the idempotent `200` replay does).

**Parameters:**

- `undefined` (, string)

**Request body** (JSON):

- `bookId` (string, uuid, required) — ID of a book previously created via POST /api/v1/books. Must belong to the API key owner.
- `count` (integer, 1–5, default 1) — Number of covers to generate (1-5). Each cover costs one credit.
- `templateId` (string) — Optional. Pin a specific V2 template. Must be a valid V2 template id (see GET /api/v1/templates); unknown ids return `invalid_template_id`.
- `prompt` (string, max 1500 chars) — Optional free-text guidance (mapped internally to the generation pipeline's mainElements/refinement note).
- `colorPalette` (string, max 200 chars) — Optional. Must be one of the predefined palettes (`custom` is not accepted via the API). Allowed: `monochrome`, `pastel`, `earth`, `muted`, `dark`, `bold`, `warm`, `cool`, `metallic`, `duotone`.
- `mood` (string, max 200 chars) — Optional. Must be one of the predefined mood values. Allowed: `intense`, `mysterious`, `romantic`, `intellectual`, `uplifting`, `epic`, `melancholic`.
- `external_ref` (string, max 200 chars) — Optional caller-supplied reference, stored on the job and echoed in responses and webhooks.
- `webhookUrl` (string, max 500 chars) — Optional per-job webhook URL. Overrides the API key's default webhook URL for this job.

**Responses:**

- `200` — Idempotent replay: a job with the same `Idempotency-Key` already exists for this API key. Includes `operation`. (`JobReplay`)
- `202` — Job accepted and queued. NOTE: this envelope has no `operation` field. (`GenerateJobQueued`)
- `400` — Validation or moderation failure. `code` is one of: `missing_book_id`, `invalid_count`, `invalid_field_type`, `field_too_long`, `invalid_field_value`, `invalid_template_id`, `content_policy_violation`. (`ErrorEnvelope`) Error codes: `missing_book_id`, `invalid_count`, `invalid_field_type`, `field_too_long`, `invalid_field_value`, `invalid_template_id`, `content_policy_violation`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Book not found or not owned by this API key's user. `code`: `book_not_found`. (`ErrorEnvelope`) Error codes: `book_not_found`.
- `422` — The book's genre/subgenre is not yet supported by the V2 generation engine. `code`: `unsupported_genre`. (`ErrorEnvelope`) Error codes: `unsupported_genre`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### GET /api/v1/covers/{coverId}

Get a cover and its asset availability

Returns the cover's metadata and, for each of the six asset types (`preview`, `full`, `audiobook_preview`, `audiobook_full`, `no_text`, `no_text_full`), whether it exists plus a 1-hour signed download URL when it does.

**Parameters:**

- `undefined` (, string)

**Responses:**

- `200` — Cover detail. (`CoverDetail`)
- `400` — `code`: `missing_cover_id` (defensive; the path parameter is normally always present). (`ErrorEnvelope`) Error codes: `missing_cover_id`.
- `401` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `500` — 

### POST /api/v1/covers/{coverId}/variations

Generate variations of an existing cover (async)

Re-renders the source cover's stored prompt to produce `count` new sibling covers. Costs `count` credits (unused credits refunded on partial/total failure). The source cover must have an associated prompt (`missing_prompt` otherwise).

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `count` (integer, 1–5, default 1) — Number of variations to generate (1-5). One credit each.
- `external_ref` (string) — Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)
- `webhookUrl` (string) — Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `variation`. (`CoverOperationJobQueued`)
- `400` — `code` is one of: `invalid_count`, `missing_prompt` (the cover has no associated prompt). (`ErrorEnvelope`) Error codes: `invalid_count`, `missing_prompt`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### POST /api/v1/covers/{coverId}/edit-image

Edit an existing cover image with a text prompt (async)

Applies an AI image edit to the cover's preview image, producing one new child cover. Costs 1 credit. The source cover must have a preview image. NOTE: the `prompt` limit on this endpoint is 1000 characters (route-local), not the 1500-character limit used by /covers/generate.

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `prompt` (string, required, max 1000 chars) — Required non-empty edit instruction. Max 1000 characters. Subject to content moderation.
- `external_ref` (string) — Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)
- `webhookUrl` (string) — Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `edit_image`. (`CoverOperationJobQueued`)
- `400` — `code` is one of: `missing_prompt`, `field_too_long` (prompt > 1000 chars), `missing_cover_image`, `content_policy_violation`. (`ErrorEnvelope`) Error codes: `missing_prompt`, `field_too_long`, `missing_cover_image`, `content_policy_violation`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### POST /api/v1/covers/{coverId}/upscale

Upscale a cover asset to print resolution (async)

Upscales `preview` -> `full`, `audiobook_preview` -> `audiobook_full`, or `no_text` -> `no_text_full`. FREE: consumes no credits (only the concurrency limit applies; there is no 402 on this endpoint). Returns `409 asset_already_exists` if the target asset already exists, and `404 asset_not_available` if the source asset has not been generated.

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `sourceAsset` (string, required) — Which asset to upscale. `preview` produces the `full` asset; `audiobook_preview` produces `audiobook_full`; `no_text` produces `no_text_full`. Allowed: `preview`, `audiobook_preview`, `no_text`.
- `external_ref` (string) — Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)
- `webhookUrl` (string) — Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `upscale`. (`CoverOperationJobQueued`)
- `400` — `code`: `invalid_source_asset` (missing or not one of `preview`, `audiobook_preview`). (`ErrorEnvelope`) Error codes: `invalid_source_asset`.
- `401` — 
- `403` — 
- `404` — `code` is one of: `asset_not_available` (source asset not generated yet), `cover_not_found` (cover missing or not owned). (`ErrorEnvelope`) Error codes: `asset_not_available`, `cover_not_found`.
- `409` — `code` is one of: `asset_already_exists` (the target asset already exists for this cover), `operation_in_progress` (an identical job for this cover is already queued or processing — poll it instead of starting another; a second run would charge again and overwrite the first result). (`ErrorEnvelope`) Error codes: `asset_already_exists`, `operation_in_progress`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### POST /api/v1/covers/{coverId}/audiobook-cover

Generate a square audiobook version of a cover (async)

Produces the `audiobook_preview` asset from the cover's preview image. Costs 1 credit. The request body is optional (a missing/invalid JSON body is treated as `{}`).

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `external_ref` (string) — Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)
- `webhookUrl` (string) — Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `audiobook_cover`. (`CoverOperationJobQueued`)
- `400` — `code`: `missing_cover_image` (the cover has no preview image). (`ErrorEnvelope`) Error codes: `missing_cover_image`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `409` — `code` is one of: `asset_already_exists` (the target asset already exists for this cover), `operation_in_progress` (an identical job for this cover is already queued or processing — poll it instead of starting another; a second run would charge again and overwrite the first result). (`ErrorEnvelope`) Error codes: `asset_already_exists`, `operation_in_progress`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### POST /api/v1/uploads

Presign a reference-image upload

Mints a short-lived (5 minute) presigned `PUT` URL for a reference image and returns the `upload_id` to pass to `POST /api/v1/covers/generate-reference`. FREE: consumes no credits. The storage key is server-generated — the client cannot choose it. Nothing about the object is trusted here: size, dimensions, and content-policy checks all run when a generation call consumes the `upload_id`, before any credit is reserved. Upload the bytes with a plain `PUT` to `upload_url` using the same `Content-Type` you declared.

**Request body** (JSON):

- `content_type` (string, required) — MIME type of the image you are going to upload. Allowed: `image/jpeg`, `image/png`, `image/webp`.
- `size_bytes` (integer) — Optional declared byte size; rejected early with `file_too_large` when it exceeds the limit returned in `max_bytes`.

**Responses:**

- `200` — Presigned upload minted. (`UploadTicket`)
- `400` — `code`: `file_too_large` (declared size_bytes over the limit). (`ErrorEnvelope`) Error codes: `file_too_large`.
- `401` — 
- `403` — 
- `415` — Missing or unsupported `content_type`. `code`: `unsupported_media_type`. (`ErrorEnvelope`) Error codes: `unsupported_media_type`.
- `500` — Server error. `code`: `internal_error`. (`ErrorEnvelope`) Error codes: `internal_error`.

### POST /api/v1/covers/generate-reference

Generate a cover from reference images (async)

Generates a NEW cover for a book from 1-4 reference images plus art direction. A vision model reads the references and writes a self-contained prompt which the text-to-image chain renders — the image model never receives the reference images, so the output is an original cover in the referenced style, not a derivative. Costs 1 credit. References come from `POST /api/v1/uploads` (`reference_upload_ids`) and/or existing covers in the account (`reference_cover_ids`). Uploads are size-, dimension-, and content-checked, and instructions are moderated, all before the credit is reserved. Covers produced this way have a null `coverStyle` (they sit outside the template system).

**Parameters:**

- `undefined` (, string)

**Request body** (JSON):

- `bookId` (string, uuid, required) — The book the new cover belongs to.
- `instructions` (string, required, max 1000 chars) — Art direction: what to take from the references and what to change.
- `reference_upload_ids` (array) — upload_ids from POST /api/v1/uploads. Combined with reference_cover_ids, 1-4 total.
- `reference_cover_ids` (array) — Public coverIds of existing covers to use as references. Combined with reference_upload_ids, 1-4 total.
- `external_ref` (string, max 200 chars) — Optional caller-supplied reference, echoed on the job and webhooks.
- `webhookUrl` (string, max 500 chars) — Optional per-job webhook URL.

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `generate_reference`. (`CoverOperationJobQueued`)
- `400` — `code` is one of: `missing_instructions`, `missing_reference_images`, `too_many_reference_images`, `invalid_upload`, `upload_not_found`, `file_too_large`, `file_too_small`, `invalid_image_dimensions`, `reference_cover_not_found`, `invalid_field_type`, `field_too_long`, `content_policy_violation` (a reference image or the instructions were blocked). No credit is reserved for any of these. (`ErrorEnvelope`) Error codes: `missing_instructions`, `missing_reference_images`, `too_many_reference_images`, `invalid_upload`, `upload_not_found`, `file_too_large`, `file_too_small`, `invalid_image_dimensions`, `reference_cover_not_found`, `invalid_field_type`, `field_too_long`, `content_policy_violation`.
- `401` — 
- `402` — 
- `403` — 
- `404` — `code` is `book_not_found` (book missing or not owned) or `reference_cover_not_found` (a referenced cover is missing, not owned, or an unpurchased premade cover). (`ErrorEnvelope`) Error codes: `book_not_found`, `reference_cover_not_found`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### POST /api/v1/covers/{coverId}/remove-text

Remove all rendered text from a cover (async)

Produces the `no_text` asset — the cover's artwork with title, author, and every other rendered word removed — from the cover's preview image. The original preview is left untouched. Costs 1 credit. Returns `409 asset_already_exists` if the `no_text` asset was already generated. The request body is optional (a missing/invalid JSON body is treated as `{}`). Upscale the result to print resolution via the upscale endpoint with `sourceAsset: "no_text"`.

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `external_ref` (string) — Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)
- `webhookUrl` (string) — Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `remove_text`. (`CoverOperationJobQueued`)
- `400` — `code`: `missing_cover_image` (the cover has no preview image). (`ErrorEnvelope`) Error codes: `missing_cover_image`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `409` — `code` is one of: `asset_already_exists` (the target asset already exists for this cover), `operation_in_progress` (an identical job for this cover is already queued or processing — poll it instead of starting another; a second run would charge again and overwrite the first result). (`ErrorEnvelope`) Error codes: `asset_already_exists`, `operation_in_progress`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### GET /api/v1/covers/{coverId}/assets/{assetType}/download

Download a cover asset (302 redirect)

Redirects (302) to a signed R2 URL for the requested asset. The signed URL expires after 3600 seconds. Follow the redirect to download the image bytes.

**Parameters:**

- `undefined` (, string)
- `assetType` (path, string, required) — Which asset to download. Allowed: `preview`, `full`, `audiobook_preview`, `audiobook_full`, `no_text`, `no_text_full`.

**Responses:**

- `302` — Redirect to a signed download URL (valid for 1 hour). No JSON body.
- `400` — `code` is one of: `missing_cover_id`, `invalid_asset_type`. (`ErrorEnvelope`) Error codes: `missing_cover_id`, `invalid_asset_type`.
- `401` — 
- `403` — 
- `404` — `code` is one of: `asset_not_available` (asset not generated yet), `cover_not_found` (cover missing or not owned). (`ErrorEnvelope`) Error codes: `asset_not_available`, `cover_not_found`.
- `500` — 

### GET /api/v1/jobs/{jobId}

Poll a job's status and result

Returns the job envelope. For `completed` jobs, all `covers[].signedUrl` values in `result` are freshly re-signed on every poll (1-hour expiry) and internal storage paths are stripped. Jobs belonging to a different API key/subscription return `404 job_not_found` (not 403).

**Parameters:**

- `jobId` (path, string, required) — Job id returned by a job-creating endpoint (also embedded in `poll_url`).

**Responses:**

- `200` — Current job state. (`Job`)
- `400` — `code`: `missing_job_id` (defensive; the path parameter is normally always present). (`ErrorEnvelope`) Error codes: `missing_job_id`.
- `401` — 
- `403` — 
- `404` — Job not found, or found but owned by a different API key/subscription. `code`: `job_not_found`. (`ErrorEnvelope`) Error codes: `job_not_found`.
- `500` — 

### GET /api/v1/templates

List active cover templates

Lists active partner-facing templates, optionally filtered by genre and subgenre. When `subgenre` is provided, each template additionally carries a `subgenreMatch` boolean indicating a dedicated subgenre fit (vs. a broader genre match). `subgenre` requires `genre`.

**Parameters:**

- `genre` (query, string) — Genre slug (e.g. `science-fiction`) or canonical value (e.g. `Science Fiction`). When given without `subgenre`, returns the union of templates serving any subgenre under the genre's bucket.
- `subgenre` (query, string) — Subgenre value; must be valid for the given `genre`. Requires `genre`.

**Responses:**

- `200` — Template list. (`TemplateList`)
- `400` — `code` is one of: `missing_genre_for_subgenre`, `invalid_genre`, `invalid_subgenre`. (`ErrorEnvelope`) Error codes: `missing_genre_for_subgenre`, `invalid_genre`, `invalid_subgenre`.
- `401` — 
- `403` — 
- `500` — Server error. `code` is `fetch_failed` or `internal_error`. (`ErrorEnvelope`) Error codes: `fetch_failed`, `internal_error`.

### GET /api/v1/books/{bookId}/covers

List a book's covers

Lists the covers generated for a book (newest first), exposing each cover's public `coverId` and per-asset availability flags. No signed URLs are returned here — use `GET /api/v1/covers/{coverId}` or the download endpoint.

**Parameters:**

- `bookId` (path, string, required) — The book id returned by POST /api/v1/books.
- `limit` (query, integer) — Max covers to return (default 100, max 200).

**Responses:**

- `200` — The book's covers. (`BookCoverList`)
- `400` — `code` is one of: `missing_book_id`, `invalid_limit`. (`ErrorEnvelope`) Error codes: `missing_book_id`, `invalid_limit`.
- `401` — 
- `403` — 
- `404` — Book not found or not owned by this API key's account. `code`: `book_not_found`. (`ErrorEnvelope`) Error codes: `book_not_found`.
- `500` — Server error. `code` is `fetch_failed` or `internal_error`. (`ErrorEnvelope`) Error codes: `fetch_failed`, `internal_error`.

### GET /api/v1/capabilities

Discover genre coverage and operation costs

Machine-readable capability report: every genre/subgenre with honest generation-coverage flags (derived from the same routing modules the generate endpoint gates on), the static per-operation credit costs, and request limits. Static per deploy.

**Responses:**

- `200` — Capability report. (`Capabilities`)
- `401` — 
- `403` — 
- `500` — Server error. `code` is `internal_error`. (`ErrorEnvelope`) Error codes: `internal_error`.

### GET /api/v1/health

Liveness probe

Public liveness check — the only endpoint that needs no API key. Reports that the API is serving requests; it does not check downstream dependencies (database, payment, or image providers), so a `200` here does not promise that a generation will succeed. Per-operation availability is reported by `GET /api/v1/capabilities`. This is the `status` target of the RFC 9727 API catalog at `/.well-known/api-catalog`. Responses are not cached.

**Responses:**

- `200` — API is serving requests. (`Health`)

### GET /api/v1/credits

Get credit balance and plan

Current credit balances (monthly + overage), plan tier, and concurrency limit for the authenticated subscription.

**Responses:**

- `200` — Credit and plan summary. (`CreditSummary`)
- `401` — 
- `403` — 
- `404` — The subscription row disappeared between auth and lookup. `code`: `subscription_not_found`. (`ErrorEnvelope`) Error codes: `subscription_not_found`.
- `500` — Server error. `code` is `internal_error`. (`ErrorEnvelope`) Error codes: `internal_error`.

### GET /api/v1/jobs

List jobs

Lists the subscription's jobs, newest first, with optional status/operation filters and keyset cursor pagination. Result payloads are omitted — poll `GET /api/v1/jobs/{jobId}` for signed cover URLs.

**Parameters:**

- `status` (query, string) — Filter by job status. Allowed: `queued`, `processing`, `completed`, `failed`.
- `operation` (query, string) — Filter by operation. Allowed: `generate`, `variation`, `edit_image`, `upscale`, `audiobook_cover`, `remove_text`, `generate_reference`, `wrap`, `wrap_extend_front`.
- `undefined` (, string)
- `undefined` (, string)

**Responses:**

- `200` — One page of jobs. (`JobList`)
- `400` — `code` is one of: `invalid_status_filter`, `invalid_operation_filter`, `invalid_limit`, `invalid_cursor`. (`ErrorEnvelope`) Error codes: `invalid_status_filter`, `invalid_operation_filter`, `invalid_limit`, `invalid_cursor`.
- `401` — 
- `403` — 
- `500` — Server error. `code` is `fetch_failed` or `internal_error`. (`ErrorEnvelope`) Error codes: `fetch_failed`, `internal_error`.

### POST /api/v1/wraps

Create a full print wrap for a cover (async)

Generates back-cover artwork from an existing cover, extends the cover art out to the full front print panel, lays the supplied text out on the back, and composites a print-ready wrap: a full-resolution flat, a preview, and a KDP-ready PDF. Costs 2 credits for `designStyle: "solid"` and 4 for the AI styles; the whole amount is refunded if the job fails. Amazon KDP only. Call `GET /api/v1/wraps/calculate` first to confirm the geometry.

**Parameters:**

- `undefined` (, string)

**Request body** (JSON):

- `coverId` (string, uuid, required) — Public id of the cover to use as the front panel.
- `trimWidth` (number, required) — Trim width in inches; must be a KDP trim size for the binding.
- `trimHeight` (number, required) — Trim height in inches.
- `pageCount` (integer, required) — Interior page count. Drives spine width. Paperback 24-828, hardcover 75-550.
- `paperType` (string, required) Allowed: `white`, `cream`, `premium_color`, `standard_color`.
- `bindingType` (string, default paperback) Allowed: `paperback`, `hardcover`.
- `designStyle` (string, default complementary) — `solid` fills the back with a flat colour (2 credits, no AI). `textured` echoes the front's motifs as a subtle pattern. `continuation` treats the front as the right half of a panorama. `complementary` designs new but cohesive artwork. Allowed: `solid`, `textured`, `continuation`, `complementary`.
- `preset` (string, default standard) — Where the text goes. `standard` = tagline + blurb + author bio; `blurb-only` = one large text area; `minimal` = tagline only. Allowed: `standard`, `blurb-only`, `minimal`.
- `solidColor` (string) — Hex colour for `designStyle: "solid"`. Omitted, the dominant colour of the front cover is sampled.
- `spineColor` (string) — Hex spine colour. Omitted, it is sampled from the back artwork.
- `customInstructions` (string, max 500 chars) — Extra art direction appended to the back-cover prompt. Ignored for `solid`.
- `tagline` (string, max 200 chars) — Short hook line, set in bold at the top of the back cover (presets `standard` and `minimal`).
- `blurb` (string, max 2000 chars) — Main back-cover description. Auto-fitted to its band; a warning is returned if it will not fit even at the minimum point size.
- `authorBio` (string, max 600 chars) — Short author biography, set below the blurb (preset `standard`).
- `spineTitle` (string, max 120 chars) — Spine lettering. Dropped with a warning when `canHaveSpineText` is false.
- `spineAuthor` (string, max 120 chars) — Author name on the spine.
- `external_ref` (string, max 200 chars)
- `webhookUrl` (string, max 500 chars)

**Responses:**

- `200` — Idempotent replay of an existing job with the same `Idempotency-Key`. (`JobReplay`)
- `202` — Job accepted and queued. `operation` is `wrap`. (`WrapJobQueued`)
- `400` — `code` is one of: `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`, `invalid_design_style`, `invalid_preset`, `invalid_color`, `missing_front_cover` (the cover has no image), `field_too_long`, `invalid_field_type`, `content_policy_violation`. (`ErrorEnvelope`) Error codes: `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`, `invalid_design_style`, `invalid_preset`, `invalid_color`, `missing_front_cover`, `field_too_long`, `invalid_field_type`, `content_policy_violation`.
- `401` — 
- `402` — 
- `403` — 
- `404` — Cover not found or not owned by this API key's user. `code`: `cover_not_found`. (`ErrorEnvelope`) Error codes: `cover_not_found`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

### GET /api/v1/wraps/calculate

Calculate KDP wrap dimensions

Print geometry for a KDP wrap: spine width, full canvas in inches and pixels at 300 DPI, safe margins, the barcode zone, and whether the spine is thick enough for text. Free, no credits, no side effects.

**Parameters:**

- `trimWidth` (query, number, required) — Trim width in inches, e.g. 6.
- `trimHeight` (query, number, required) — Trim height in inches, e.g. 9.
- `pageCount` (query, integer, required) — Interior page count.
- `paperType` (query, string, required) Allowed: `white`, `cream`, `premium_color`, `standard_color`.
- `bindingType` (query, string) Allowed: `paperback`, `hardcover`.

**Responses:**

- `200` — Print geometry for the requested book. (`WrapDimensions`)
- `400` — `code` is one of: `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`. (`ErrorEnvelope`) Error codes: `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`.
- `401` — 
- `403` — 
- `500` — 

### GET /api/v1/wraps/{wrapId}

Get a wrap and re-sign its assets

Returns a stored wrap with fresh one-hour signed URLs. Wraps created in the browser wrap editor are readable here too, and vice versa.

**Parameters:**

- `undefined` (, string)

**Responses:**

- `200` — The wrap design and its assets. (`WrapDetail`)
- `401` — 
- `403` — 
- `404` — Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`. (`ErrorEnvelope`) Error codes: `wrap_not_found`.
- `500` — 

### POST /api/v1/wraps/{wrapId}/render

Re-typeset an existing wrap (free, synchronous)

Re-lays the back-cover text on the artwork this wrap already has and returns fresh assets. No model runs, so it is free and returns 200 directly rather than a job to poll — fixing a typo should not cost a second wrap. Send text fields to re-run a preset, or a full `layoutState` to render a layout edited in the browser wrap editor. `spineColor` and, on a `solid` wrap, `solidColor` are free here too: a solid back panel is a flat fill, so changing its colour needs no model and never costs a second wrap.

**Parameters:**

- `undefined` (, string)

**Request body** (JSON):

- `preset` (string, default standard) — Ignored when `layoutState` is supplied. Allowed: `standard`, `blurb-only`, `minimal`.
- `tagline` (string, max 200 chars) — Short hook line, set in bold at the top of the back cover (presets `standard` and `minimal`).
- `blurb` (string, max 2000 chars) — Main back-cover description. Auto-fitted to its band; a warning is returned if it will not fit even at the minimum point size.
- `authorBio` (string, max 600 chars) — Short author biography, set below the blurb (preset `standard`).
- `spineTitle` (string, max 120 chars) — Spine lettering. Dropped with a warning when `canHaveSpineText` is false.
- `spineAuthor` (string, max 120 chars) — Author name on the spine.
- `spineColor` (string) — Hex spine colour. Omitted, the wrap's stored colour is kept.
- `solidColor` (string) — New hex colour for a solid back cover, e.g. `#1A2B3C`. Free, like the rest of this endpoint — a solid back panel is a flat fill, so repainting it runs no model — and it is persisted, so later renders keep it. Valid only on a wrap created with `designStyle: solid`; on a wrap whose back is AI artwork the colour comes from the art itself and this returns `back_color_requires_solid_style`.
- `layoutState` (object) — A complete editor-schema layout (as returned in `layout_state` by GET /api/v1/wraps/{wrapId}). Takes precedence over the text fields and the preset.

**Responses:**

- `200` — Re-rendered assets and the layout report. (`WrapRenderResult`)
- `400` — `code` is one of: `invalid_preset`, `invalid_layout`, `invalid_color`, `missing_back_art` (the wrap has no artwork yet), `missing_front_cover`, `field_too_long`, `invalid_field_type`, `content_policy_violation`. (`ErrorEnvelope`) Error codes: `invalid_preset`, `invalid_layout`, `invalid_color`, `missing_back_art`, `missing_front_cover`, `field_too_long`, `invalid_field_type`, `content_policy_violation`.
- `401` — 
- `403` — 
- `404` — Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`. (`ErrorEnvelope`) Error codes: `wrap_not_found`.
- `500` — 

### POST /api/v1/wraps/{wrapId}/extend-front

Re-run the front-panel AI extension (free, async)

Re-runs the AI outpaint that extends the front cover artwork to the full print panel, then recomposes the wrap with its existing back artwork, layout, and spine. Use it when the extension came back with a seam, a framed-picture look, or an unfilled band (see the `front_outpaint_incomplete` warning on the wrap job). Free — the wrap was already paid for — but it runs an AI chain, so it returns `202` and a job to poll. The equivalent of the wrap editor's "re-extend front" action.

**Parameters:**

- `undefined` (, string)
- `undefined` (, string)

**Request body** (JSON):

- `external_ref` (string, max 255 chars) — Your own reference id, echoed on the job and its webhooks.
- `webhookUrl` (string, max 2000 chars) — Overrides the API key's default webhook URL for this job.

**Responses:**

- `200` — Idempotent replay: a job already exists for this Idempotency-Key. (`JobReplay`)
- `202` — Job accepted. Poll `poll_url`; the completed job's `result` is a `WrapJobResult` with fresh assets. (`WrapExtendJobQueued`)
- `400` — `code` is one of: `missing_back_art` (the wrap has no artwork yet), `missing_front_cover` (the cover behind the wrap no longer has an image), `field_too_long`, `invalid_field_type`, or a geometry code if the wrap's stored trim/page setup no longer passes the provider's rules. (`ErrorEnvelope`) Error codes: `missing_back_art`, `missing_front_cover`, `field_too_long`, `invalid_field_type`, `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`.
- `401` — 
- `403` — 
- `404` — Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`. (`ErrorEnvelope`) Error codes: `wrap_not_found`.
- `429` — 
- `500` — Server error. `code` is `job_creation_failed` or `internal_error`. Nothing was charged. (`ErrorEnvelope`) Error codes: `job_creation_failed`, `internal_error`.

## Webhooks

### jobCompleted

job.completed

Sent to the job's `webhookUrl` (falling back to the API key's default webhook URL) when a job completes. For `generate` jobs the payload has NO `operation` field and `data` is `{ bookId, covers[] }`; for cover-operation jobs (`variation`, `edit_image`, `upscale`, `audiobook_cover`) the payload includes `operation` and `data` is the operation's result (`sourceCoverId` + `covers[]` for variation/edit_image; `covers[]` with an `asset` field for upscale/audiobook_cover). Internal `_storagePath` fields are stripped. Signed with the API key's webhook secret (see partner webhook docs).

### jobFailed

job.failed

Sent when a job fails. For `generate` jobs there is NO `operation` field; cover-operation jobs include it. Reserved credits are refunded on failure.
