# 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`, `get_generation_status`, `list_templates_for_genre`, `get_credit_balance`, `list_books`, `download_cover`, `upscale_cover` (free, no credits), `edit_cover`, `create_audiobook_cover`, `calculate_kdp_wrap` (free, no credits). Generation and follow-on operations are async — poll `get_generation_status` for 1-hour signed download URLs.

## 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 four asset types (`preview`, `full`, `audiobook_preview`, `audiobook_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` or `audiobook_preview` -> `audiobook_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`. Allowed: `preview`, `audiobook_preview`.
- `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` — The target asset already exists for this cover. `code`: `asset_already_exists`. (`ErrorEnvelope`) Error codes: `asset_already_exists`.
- `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`.
- `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`.

**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/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`.
- `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`.

## 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.
