{"openapi":"3.1.0","info":{"title":"BeYourCover Partner API","version":"1.0.0","description":"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 } }`."},"servers":[{"url":"https://beyourcover.com","description":"Production. All paths below are relative to the site origin."}],"security":[{"ApiKeyBearer":[]}],"tags":[{"name":"Books"},{"name":"Covers"},{"name":"Wraps","description":"Full print wrap covers (back + spine + front) for Amazon KDP."},{"name":"Jobs"},{"name":"Templates"},{"name":"Account","description":"Subscription credits and API capability discovery."},{"name":"Service","description":"Unauthenticated service discovery and health. Linked from the RFC 9727 API catalog at `/.well-known/api-catalog`."}],"paths":{"/api/v1/books":{"post":{"tags":["Books"],"operationId":"createBook","summary":"Create a book record","description":"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":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","genre"],"properties":{"title":{"type":"string","maxLength":100,"description":"Book title. Required. Rendered on the cover, so Arabic and Hindi/Devanagari scripts are rejected (`unsupported_characters`)."},"author":{"type":"string","maxLength":100,"description":"Author name. Required unless `genre` is `Planner & Journal` / `planner-journal`. Same script restrictions as `title`."},"subtitle":{"type":"string","maxLength":300,"description":"Optional subtitle. Same script restrictions as `title`."},"genre":{"type":"string","description":"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":{"type":"string","maxLength":100,"description":"Optional subgenre value. If omitted for genres that have subgenres, the server auto-detects one."},"summary":{"type":"string","maxLength":1000,"description":"Optional plot/content summary used to inform generation."},"target_audience":{"type":"string","enum":["children","middle-grade","young-adult","new-adult","adult"],"maxLength":500,"description":"Optional. Must be one of the predefined audience values."},"external_ref":{"type":"string","maxLength":200,"description":"Optional caller-supplied reference. Echoed back in the response (not stored on the book)."},"planner_year":{"type":"string","description":"Optional. Only meaningful for the planner genre. Must be a string if provided (type-checked; no length limit is enforced in code)."}}}}}},"responses":{"200":{"description":"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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookCreated"}}}},"201":{"description":"Book created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookCreated"}}}},"400":{"description":"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`.","x-error-codes":["missing_required_fields","invalid_field_type","field_too_long","unsupported_characters","invalid_field_value","content_policy_violation","invalid_genre"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"Server error. `code` is `book_creation_failed` (insert failed) or `internal_error`.","x-error-codes":["book_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"tags":["Books"],"operationId":"listBooks","summary":"List books","description":"Lists books owned by the API key's account, newest first, with keyset cursor pagination.","parameters":[{"$ref":"#/components/parameters/PageLimit"},{"$ref":"#/components/parameters/PageCursor"}],"responses":{"200":{"description":"One page of books.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookList"}}}},"400":{"description":"`code` is one of: `invalid_limit`, `invalid_cursor`.","x-error-codes":["invalid_limit","invalid_cursor"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"Server error. `code` is `fetch_failed` or `internal_error`.","x-error-codes":["fetch_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/generate":{"post":{"tags":["Covers"],"operationId":"generateCovers","summary":"Generate covers for a book (async)","description":"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":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["bookId"],"properties":{"bookId":{"type":"string","format":"uuid","description":"ID of a book previously created via POST /api/v1/books. Must belong to the API key owner."},"count":{"type":"integer","minimum":1,"maximum":5,"default":1,"description":"Number of covers to generate (1-5). Each cover costs one credit."},"templateId":{"type":"string","description":"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":{"type":"string","maxLength":1500,"description":"Optional free-text guidance (mapped internally to the generation pipeline's mainElements/refinement note)."},"colorPalette":{"type":"string","maxLength":200,"enum":["monochrome","pastel","earth","muted","dark","bold","warm","cool","metallic","duotone"],"description":"Optional. Must be one of the predefined palettes (`custom` is not accepted via the API)."},"mood":{"type":"string","maxLength":200,"enum":["intense","mysterious","romantic","intellectual","uplifting","epic","melancholic"],"description":"Optional. Must be one of the predefined mood values."},"external_ref":{"type":"string","maxLength":200,"description":"Optional caller-supplied reference, stored on the job and echoed in responses and webhooks."},"webhookUrl":{"type":"string","maxLength":500,"description":"Optional per-job webhook URL. Overrides the API key's default webhook URL for this job."}}}}}},"responses":{"200":{"description":"Idempotent replay: a job with the same `Idempotency-Key` already exists for this API key. Includes `operation`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. NOTE: this envelope has no `operation` field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateJobQueued"}}}},"400":{"description":"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`.","x-error-codes":["missing_book_id","invalid_count","invalid_field_type","field_too_long","invalid_field_value","invalid_template_id","content_policy_violation"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Book not found or not owned by this API key's user. `code`: `book_not_found`.","x-error-codes":["book_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"The book's genre/subgenre is not yet supported by the V2 generation engine. `code`: `unsupported_genre`.","x-error-codes":["unsupported_genre"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}":{"get":{"tags":["Covers"],"operationId":"getCover","summary":"Get a cover and its asset availability","description":"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":[{"$ref":"#/components/parameters/CoverId"}],"responses":{"200":{"description":"Cover detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverDetail"}}}},"400":{"description":"`code`: `missing_cover_id` (defensive; the path parameter is normally always present).","x-error-codes":["missing_cover_id"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/covers/{coverId}/variations":{"post":{"tags":["Covers"],"operationId":"createVariations","summary":"Generate variations of an existing cover (async)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","minimum":1,"maximum":5,"default":1,"description":"Number of variations to generate (1-5). One credit each."},"external_ref":{"type":"string","description":"Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)"},"webhookUrl":{"type":"string","description":"Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)"}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `variation`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`code` is one of: `invalid_count`, `missing_prompt` (the cover has no associated prompt).","x-error-codes":["invalid_count","missing_prompt"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}/edit-image":{"post":{"tags":["Covers"],"operationId":"editCoverImage","summary":"Edit an existing cover image with a text prompt (async)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","minLength":1,"maxLength":1000,"description":"Required non-empty edit instruction. Max 1000 characters. Subject to content moderation."},"external_ref":{"type":"string","description":"Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)"},"webhookUrl":{"type":"string","description":"Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)"}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `edit_image`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`code` is one of: `missing_prompt`, `field_too_long` (prompt > 1000 chars), `missing_cover_image`, `content_policy_violation`.","x-error-codes":["missing_prompt","field_too_long","missing_cover_image","content_policy_violation"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}/upscale":{"post":{"tags":["Covers"],"operationId":"upscaleCover","summary":"Upscale a cover asset to print resolution (async)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sourceAsset"],"properties":{"sourceAsset":{"type":"string","enum":["preview","audiobook_preview","no_text"],"description":"Which asset to upscale. `preview` produces the `full` asset; `audiobook_preview` produces `audiobook_full`; `no_text` produces `no_text_full`."},"external_ref":{"type":"string","description":"Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)"},"webhookUrl":{"type":"string","description":"Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)"}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `upscale`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`code`: `invalid_source_asset` (missing or not one of `preview`, `audiobook_preview`).","x-error-codes":["invalid_source_asset"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`code` is one of: `asset_not_available` (source asset not generated yet), `cover_not_found` (cover missing or not owned).","x-error-codes":["asset_not_available","cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"`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).","x-error-codes":["asset_already_exists","operation_in_progress"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}/audiobook-cover":{"post":{"tags":["Covers"],"operationId":"createAudiobookCover","summary":"Generate a square audiobook version of a cover (async)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"external_ref":{"type":"string","description":"Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)"},"webhookUrl":{"type":"string","description":"Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)"}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `audiobook_cover`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`code`: `missing_cover_image` (the cover has no preview image).","x-error-codes":["missing_cover_image"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"`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).","x-error-codes":["asset_already_exists","operation_in_progress"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/uploads":{"post":{"tags":["Covers"],"operationId":"createUpload","summary":"Presign a reference-image upload","description":"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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content_type"],"properties":{"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp"],"description":"MIME type of the image you are going to upload."},"size_bytes":{"type":"integer","description":"Optional declared byte size; rejected early with `file_too_large` when it exceeds the limit returned in `max_bytes`."}}}}}},"responses":{"200":{"description":"Presigned upload minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadTicket"}}}},"400":{"description":"`code`: `file_too_large` (declared size_bytes over the limit).","x-error-codes":["file_too_large"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"415":{"description":"Missing or unsupported `content_type`. `code`: `unsupported_media_type`.","x-error-codes":["unsupported_media_type"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error. `code`: `internal_error`.","x-error-codes":["internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/generate-reference":{"post":{"tags":["Covers"],"operationId":"generateCoverFromReference","summary":"Generate a cover from reference images (async)","description":"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":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["bookId","instructions"],"properties":{"bookId":{"type":"string","format":"uuid","description":"The book the new cover belongs to."},"instructions":{"type":"string","maxLength":1000,"description":"Art direction: what to take from the references and what to change."},"reference_upload_ids":{"type":"array","items":{"type":"string"},"description":"upload_ids from POST /api/v1/uploads. Combined with reference_cover_ids, 1-4 total."},"reference_cover_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Public coverIds of existing covers to use as references. Combined with reference_upload_ids, 1-4 total."},"external_ref":{"type":"string","maxLength":200,"description":"Optional caller-supplied reference, echoed on the job and webhooks."},"webhookUrl":{"type":"string","maxLength":500,"description":"Optional per-job webhook URL."}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `generate_reference`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`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.","x-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"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`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).","x-error-codes":["book_not_found","reference_cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}/remove-text":{"post":{"tags":["Covers"],"operationId":"removeCoverText","summary":"Remove all rendered text from a cover (async)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"external_ref":{"type":"string","description":"Optional caller-supplied reference. (Length/type limits are not enforced on this endpoint in code.)"},"webhookUrl":{"type":"string","description":"Optional per-job webhook URL. (Length/type limits are not enforced on this endpoint in code.)"}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `remove_text`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverOperationJobQueued"}}}},"400":{"description":"`code`: `missing_cover_image` (the cover has no preview image).","x-error-codes":["missing_cover_image"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"`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).","x-error-codes":["asset_already_exists","operation_in_progress"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. The reserved credit is refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/covers/{coverId}/assets/{assetType}/download":{"get":{"tags":["Covers"],"operationId":"downloadCoverAsset","summary":"Download a cover asset (302 redirect)","description":"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":[{"$ref":"#/components/parameters/CoverId"},{"name":"assetType","in":"path","required":true,"schema":{"type":"string","enum":["preview","full","audiobook_preview","audiobook_full","no_text","no_text_full"]},"description":"Which asset to download."}],"responses":{"302":{"description":"Redirect to a signed download URL (valid for 1 hour). No JSON body.","headers":{"Location":{"description":"Time-limited signed R2 URL for the asset image.","schema":{"type":"string","format":"uri"}}}},"400":{"description":"`code` is one of: `missing_cover_id`, `invalid_asset_type`.","x-error-codes":["missing_cover_id","invalid_asset_type"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`code` is one of: `asset_not_available` (asset not generated yet), `cover_not_found` (cover missing or not owned).","x-error-codes":["asset_not_available","cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/jobs/{jobId}":{"get":{"tags":["Jobs"],"operationId":"getJob","summary":"Poll a job's status and result","description":"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":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Job id returned by a job-creating endpoint (also embedded in `poll_url`)."}],"responses":{"200":{"description":"Current job state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"400":{"description":"`code`: `missing_job_id` (defensive; the path parameter is normally always present).","x-error-codes":["missing_job_id"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Job not found, or found but owned by a different API key/subscription. `code`: `job_not_found`.","x-error-codes":["job_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/templates":{"get":{"tags":["Templates"],"operationId":"listTemplates","summary":"List active cover templates","description":"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":[{"name":"genre","in":"query","required":false,"schema":{"type":"string"},"description":"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."},{"name":"subgenre","in":"query","required":false,"schema":{"type":"string"},"description":"Subgenre value; must be valid for the given `genre`. Requires `genre`."}],"responses":{"200":{"description":"Template list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateList"}}}},"400":{"description":"`code` is one of: `missing_genre_for_subgenre`, `invalid_genre`, `invalid_subgenre`.","x-error-codes":["missing_genre_for_subgenre","invalid_genre","invalid_subgenre"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"Server error. `code` is `fetch_failed` or `internal_error`.","x-error-codes":["fetch_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/books/{bookId}/covers":{"get":{"tags":["Covers"],"operationId":"listBookCovers","summary":"List a book's covers","description":"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":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The book id returned by POST /api/v1/books."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Max covers to return (default 100, max 200)."}],"responses":{"200":{"description":"The book's covers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookCoverList"}}}},"400":{"description":"`code` is one of: `missing_book_id`, `invalid_limit`.","x-error-codes":["missing_book_id","invalid_limit"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Book not found or not owned by this API key's account. `code`: `book_not_found`.","x-error-codes":["book_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error. `code` is `fetch_failed` or `internal_error`.","x-error-codes":["fetch_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/capabilities":{"get":{"tags":["Account"],"operationId":"getCapabilities","summary":"Discover genre coverage and operation costs","description":"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":{"description":"Capability report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Capabilities"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"Server error. `code` is `internal_error`.","x-error-codes":["internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/health":{"get":{"tags":["Service"],"operationId":"getHealth","summary":"Liveness probe","description":"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.","security":[],"responses":{"200":{"description":"API is serving requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/api/v1/credits":{"get":{"tags":["Account"],"operationId":"getCredits","summary":"Get credit balance and plan","description":"Current credit balances (monthly + overage), plan tier, and concurrency limit for the authenticated subscription.","responses":{"200":{"description":"Credit and plan summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The subscription row disappeared between auth and lookup. `code`: `subscription_not_found`.","x-error-codes":["subscription_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error. `code` is `internal_error`.","x-error-codes":["internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/jobs":{"get":{"tags":["Jobs"],"operationId":"listJobs","summary":"List jobs","description":"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":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["queued","processing","completed","failed"]},"description":"Filter by job status."},{"name":"operation","in":"query","required":false,"schema":{"type":"string","enum":["generate","variation","edit_image","upscale","audiobook_cover","remove_text","generate_reference","wrap","wrap_extend_front"]},"description":"Filter by operation."},{"$ref":"#/components/parameters/PageLimit"},{"$ref":"#/components/parameters/PageCursor"}],"responses":{"200":{"description":"One page of jobs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobList"}}}},"400":{"description":"`code` is one of: `invalid_status_filter`, `invalid_operation_filter`, `invalid_limit`, `invalid_cursor`.","x-error-codes":["invalid_status_filter","invalid_operation_filter","invalid_limit","invalid_cursor"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"Server error. `code` is `fetch_failed` or `internal_error`.","x-error-codes":["fetch_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/wraps":{"post":{"tags":["Wraps"],"operationId":"createWrap","summary":"Create a full print wrap for a cover (async)","description":"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":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["coverId","trimWidth","trimHeight","pageCount","paperType"],"properties":{"coverId":{"type":"string","format":"uuid","description":"Public id of the cover to use as the front panel."},"trimWidth":{"type":"number","description":"Trim width in inches; must be a KDP trim size for the binding."},"trimHeight":{"type":"number","description":"Trim height in inches."},"pageCount":{"type":"integer","description":"Interior page count. Drives spine width. Paperback 24-828, hardcover 75-550."},"paperType":{"type":"string","enum":["white","cream","premium_color","standard_color"]},"bindingType":{"type":"string","enum":["paperback","hardcover"],"default":"paperback"},"designStyle":{"type":"string","enum":["solid","textured","continuation","complementary"],"default":"complementary","description":"`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."},"preset":{"type":"string","enum":["standard","blurb-only","minimal"],"default":"standard","description":"Where the text goes. `standard` = tagline + blurb + author bio; `blurb-only` = one large text area; `minimal` = tagline only."},"solidColor":{"type":"string","description":"Hex colour for `designStyle: \"solid\"`. Omitted, the dominant colour of the front cover is sampled."},"spineColor":{"type":"string","description":"Hex spine colour. Omitted, it is sampled from the back artwork."},"customInstructions":{"type":"string","maxLength":500,"description":"Extra art direction appended to the back-cover prompt. Ignored for `solid`."},"tagline":{"type":"string","maxLength":200,"description":"Short hook line, set in bold at the top of the back cover (presets `standard` and `minimal`)."},"blurb":{"type":"string","maxLength":2000,"description":"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":{"type":"string","maxLength":600,"description":"Short author biography, set below the blurb (preset `standard`)."},"spineTitle":{"type":"string","maxLength":120,"description":"Spine lettering. Dropped with a warning when `canHaveSpineText` is false."},"spineAuthor":{"type":"string","maxLength":120,"description":"Author name on the spine."},"external_ref":{"type":"string","maxLength":200},"webhookUrl":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Idempotent replay of an existing job with the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted and queued. `operation` is `wrap`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WrapJobQueued"}}}},"400":{"description":"`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`.","x-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"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cover not found or not owned by this API key's user. `code`: `cover_not_found`.","x-error-codes":["cover_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. Reserved credits are refunded.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/wraps/calculate":{"get":{"tags":["Wraps"],"operationId":"calculateWrapDimensions","summary":"Calculate KDP wrap dimensions","description":"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":[{"name":"trimWidth","in":"query","required":true,"schema":{"type":"number"},"description":"Trim width in inches, e.g. 6."},{"name":"trimHeight","in":"query","required":true,"schema":{"type":"number"},"description":"Trim height in inches, e.g. 9."},{"name":"pageCount","in":"query","required":true,"schema":{"type":"integer"},"description":"Interior page count."},{"name":"paperType","in":"query","required":true,"schema":{"type":"string","enum":["white","cream","premium_color","standard_color"]}},{"name":"bindingType","in":"query","required":false,"schema":{"type":"string","enum":["paperback","hardcover"],"default":"paperback"}}],"responses":{"200":{"description":"Print geometry for the requested book.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WrapDimensions"}}}},"400":{"description":"`code` is one of: `invalid_trim_size`, `invalid_page_count`, `invalid_paper_type`, `invalid_binding_type`.","x-error-codes":["invalid_trim_size","invalid_page_count","invalid_paper_type","invalid_binding_type"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/wraps/{wrapId}":{"get":{"tags":["Wraps"],"operationId":"getWrap","summary":"Get a wrap and re-sign its assets","description":"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":[{"$ref":"#/components/parameters/WrapId"}],"responses":{"200":{"description":"The wrap design and its assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WrapDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`.","x-error-codes":["wrap_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/wraps/{wrapId}/render":{"post":{"tags":["Wraps"],"operationId":"renderWrap","summary":"Re-typeset an existing wrap (free, synchronous)","description":"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":[{"$ref":"#/components/parameters/WrapId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"preset":{"type":"string","enum":["standard","blurb-only","minimal"],"default":"standard","description":"Ignored when `layoutState` is supplied."},"tagline":{"type":"string","maxLength":200,"description":"Short hook line, set in bold at the top of the back cover (presets `standard` and `minimal`)."},"blurb":{"type":"string","maxLength":2000,"description":"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":{"type":"string","maxLength":600,"description":"Short author biography, set below the blurb (preset `standard`)."},"spineTitle":{"type":"string","maxLength":120,"description":"Spine lettering. Dropped with a warning when `canHaveSpineText` is false."},"spineAuthor":{"type":"string","maxLength":120,"description":"Author name on the spine."},"spineColor":{"type":"string","description":"Hex spine colour. Omitted, the wrap's stored colour is kept."},"solidColor":{"type":"string","description":"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":{"type":"object","description":"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":{"description":"Re-rendered assets and the layout report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WrapRenderResult"}}}},"400":{"description":"`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`.","x-error-codes":["invalid_preset","invalid_layout","invalid_color","missing_back_art","missing_front_cover","field_too_long","invalid_field_type","content_policy_violation"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`.","x-error-codes":["wrap_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/wraps/{wrapId}/extend-front":{"post":{"tags":["Wraps"],"operationId":"extendWrapFront","summary":"Re-run the front-panel AI extension (free, async)","description":"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":[{"$ref":"#/components/parameters/WrapId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"external_ref":{"type":"string","maxLength":255,"description":"Your own reference id, echoed on the job and its webhooks."},"webhookUrl":{"type":"string","maxLength":2000,"description":"Overrides the API key's default webhook URL for this job."}}}}}},"responses":{"200":{"description":"Idempotent replay: a job already exists for this Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReplay"}}}},"202":{"description":"Job accepted. Poll `poll_url`; the completed job's `result` is a `WrapJobResult` with fresh assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WrapExtendJobQueued"}}}},"400":{"description":"`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.","x-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"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Wrap not found or not owned by this API key's user. `code`: `wrap_not_found`.","x-error-codes":["wrap_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ConcurrencyLimit"},"500":{"description":"Server error. `code` is `job_creation_failed` or `internal_error`. Nothing was charged.","x-error-codes":["job_creation_failed","internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"webhooks":{"jobCompleted":{"post":{"summary":"job.completed","description":"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).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookJobCompleted"}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge receipt."}}}},"jobFailed":{"post":{"summary":"job.failed","description":"Sent when a job fails. For `generate` jobs there is NO `operation` field; cover-operation jobs include it. Reserved credits are refunded on failure.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookJobFailed"}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge receipt."}}}}},"components":{"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"byc_sk_{32 hex chars}","description":"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`."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional idempotency key. If a resource/job was already created with the same key (scoped to the API key for jobs, to the user for books), the endpoint returns `200` with the existing resource instead of creating a duplicate. Concurrent duplicates are also collapsed via a unique-constraint retry."},"CoverId":{"name":"coverId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The cover's public id (UUID) as returned in job results (`covers[].coverId`)."},"PageLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size (default 20, max 100)."},"PageCursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from a previous response's `next_cursor`. Omit for the first page."},"WrapId":{"name":"wrapId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The wrap's id, returned as `wrapId` in the wrap job result."}},"responses":{"Unauthorized":{"description":"Missing/malformed API key, unknown or inactive key, or inactive subscription detected during credit checks. `code`: `unauthorized`.","x-error-codes":["unauthorized"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"The API key is valid but its owner has no active enterprise subscription. `code`: `forbidden`.","x-error-codes":["forbidden"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InsufficientCredits":{"description":"Not enough credits (monthly + overage) for the requested operation. `code`: `insufficient_credits`.","x-error-codes":["insufficient_credits"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ConcurrencyLimit":{"description":"The subscription's concurrent-job limit is reached (jobs with status `queued` or `processing` count). `code`: `concurrency_limit`.","x-error-codes":["concurrency_limit"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error. `code`: `internal_error`.","x-error-codes":["internal_error"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code (snake_case)."},"message":{"type":"string","description":"Human-readable explanation. For 500s this is always a generic message."},"details":{"type":"object","description":"Optional machine-readable recovery information. 402 responses carry { balance, required, billing_url }; unknown-path 404s carry absolute URLs for the OpenAPI spec, documentation, API catalog, authentication guide and health probe."}}}},"description":"Uniform error envelope used by every non-2xx JSON response."},"BookCreated":{"type":"object","required":["bookId","subgenre","external_ref","created_at"],"properties":{"bookId":{"type":"string","format":"uuid"},"subgenre":{"type":["string","null"],"description":"The stored subgenre: the caller-provided value, an auto-detected one, or null."},"external_ref":{"type":["string","null"],"description":"Echo of the request's external_ref (not persisted on the book)."},"created_at":{"type":"string","format":"date-time"}}},"GenerateJobQueued":{"type":"object","required":["jobId","status","external_ref","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"queued"},"external_ref":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"poll_url":{"type":"string","description":"Relative URL to poll, e.g. `/api/v1/jobs/{jobId}`.","examples":["/api/v1/jobs/6f1c2a34-...."]}},"description":"202 envelope from POST /api/v1/covers/generate. Note: no `operation` field (unlike the other job-creating endpoints)."},"CoverOperationJobQueued":{"type":"object","required":["jobId","status","operation","external_ref","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","const":"queued"},"operation":{"type":"string","enum":["variation","edit_image","upscale","audiobook_cover"]},"external_ref":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"poll_url":{"type":"string","description":"Relative URL to poll, e.g. `/api/v1/jobs/{jobId}`."}},"description":"202 envelope from the cover-operation endpoints (variations, edit-image, upscale, audiobook-cover)."},"JobReplay":{"type":"object","required":["jobId","status","operation","external_ref","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","completed","failed"],"description":"Current status of the previously created job."},"operation":{"type":"string","enum":["generate","variation","edit_image","upscale","audiobook_cover"]},"external_ref":{"type":["string","null"],"description":"The external_ref stored on the ORIGINAL job."},"created_at":{"type":"string","format":"date-time"},"poll_url":{"type":"string"}},"description":"200 envelope returned when an Idempotency-Key matches an existing job. Unlike the fresh 202, `status` reflects the job's current state and `operation` is always present."},"Job":{"type":"object","required":["jobId","status","operation","external_ref","credits_used","created_at","started_at","completed_at","result","error"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"operation":{"type":"string","enum":["generate","variation","edit_image","upscale","audiobook_cover","remove_text","generate_reference","wrap","wrap_extend_front"]},"external_ref":{"type":["string","null"]},"credits_used":{"type":["integer","null"],"description":"Credits actually consumed (set on completion; partial failures reduce it and refund the difference)."},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"result":{"oneOf":[{"$ref":"#/components/schemas/GenerateJobResult"},{"$ref":"#/components/schemas/CoverOperationJobResult"},{"$ref":"#/components/schemas/AssetJobResult"},{"$ref":"#/components/schemas/WrapJobResult"},{"type":"null"}],"description":"Null until the job completes. Shape depends on `operation`: `generate`/`generate_reference` -> GenerateJobResult; `variation`/`edit_image` -> CoverOperationJobResult; `upscale`/`audiobook_cover`/`remove_text` -> AssetJobResult; `wrap`/`wrap_extend_front` -> WrapJobResult. On each poll of a completed job, signed URLs and their `expiresAt` are refreshed."},"error":{"type":["string","null"],"description":"Failure message when status is `failed` (raw error_message string, not an error envelope)."}}},"GenerateJobResult":{"type":"object","required":["bookId","covers"],"properties":{"bookId":{"type":"string","format":"uuid"},"covers":{"type":"array","items":{"$ref":"#/components/schemas/GeneratedCover"}}},"description":"Result payload for `generate` jobs. Only successfully generated covers are included; `credits_used` reflects the successful count."},"CoverOperationJobResult":{"type":"object","required":["sourceCoverId","covers"],"properties":{"sourceCoverId":{"type":"string","format":"uuid","description":"Public id of the cover the operation was applied to."},"covers":{"type":"array","items":{"$ref":"#/components/schemas/GeneratedCover"}}},"description":"Result payload for `variation` and `edit_image` jobs. Each entry is a NEW child cover."},"AssetJobResult":{"type":"object","required":["covers"],"properties":{"covers":{"type":"array","items":{"$ref":"#/components/schemas/CoverAssetResult"}}},"description":"Result payload for `upscale` and `audiobook_cover` jobs. The new asset is attached to the SAME cover (coverId is the source cover's public id)."},"GeneratedCover":{"type":"object","required":["coverId","signedUrl","expiresAt"],"properties":{"coverId":{"type":"string","format":"uuid","description":"Public id of the cover. Use with the /api/v1/covers/{coverId} endpoints."},"signedUrl":{"type":"string","format":"uri","description":"Time-limited signed URL for the preview image (1 hour)."},"expiresAt":{"type":"string","format":"date-time"},"coverStyle":{"type":"string","description":"Template/style id used to render the cover (e.g. a V2 template id)."},"prompt":{"type":"string","description":"The generation prompt stored for this cover."}}},"CoverAssetResult":{"type":"object","required":["coverId","asset","signedUrl","expiresAt"],"properties":{"coverId":{"type":"string","format":"uuid","description":"Public id of the (source) cover the asset belongs to."},"asset":{"type":"string","enum":["full","audiobook_preview","audiobook_full","no_text","no_text_full"],"description":"Which asset was produced: `full` (upscale of preview), `audiobook_full` (upscale of audiobook_preview), `audiobook_preview` (audiobook_cover jobs), `no_text` (remove_text jobs), or `no_text_full` (upscale of no_text)."},"signedUrl":{"type":"string","format":"uri"},"expiresAt":{"type":"string","format":"date-time"}}},"CoverDetail":{"type":"object","required":["coverId","bookId","coverStyle","assets"],"properties":{"coverId":{"type":"string","format":"uuid","description":"The cover's public id."},"bookId":{"type":"string","format":"uuid"},"coverStyle":{"type":["string","null"]},"assets":{"type":"object","required":["preview","full","audiobook_preview","audiobook_full","no_text","no_text_full"],"properties":{"preview":{"$ref":"#/components/schemas/AssetAvailability"},"full":{"$ref":"#/components/schemas/AssetAvailability"},"audiobook_preview":{"$ref":"#/components/schemas/AssetAvailability"},"audiobook_full":{"$ref":"#/components/schemas/AssetAvailability"},"no_text":{"$ref":"#/components/schemas/AssetAvailability"},"no_text_full":{"$ref":"#/components/schemas/AssetAvailability"}}}}},"AssetAvailability":{"type":"object","required":["available"],"properties":{"available":{"type":"boolean"},"signedUrl":{"type":"string","format":"uri","description":"Present only when available is true. Signed URL valid for 1 hour."},"expiresAt":{"type":"string","format":"date-time","description":"Present only when available is true."}}},"Template":{"type":"object","required":["id","name","family","templateType","genreBuckets","previewImage"],"properties":{"id":{"type":"string","description":"Template id; usable as `templateId` in POST /api/v1/covers/generate."},"name":{"type":"string"},"family":{"type":["string","null"]},"templateType":{"type":["string","null"]},"genreBuckets":{"type":["array","null"],"items":{"type":"string"},"description":"Bucket keys the template serves, typically in `Bucket:subgenre` form."},"previewImage":{"type":["string","null"]},"subgenreMatch":{"type":"boolean","description":"Only present when the request included a `subgenre` query parameter. True when the template is a dedicated match for that subgenre."}}},"TemplateList":{"type":"object","required":["templates","total"],"properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"total":{"type":"integer"}}},"WebhookJobCompleted":{"type":"object","required":["event","jobId","external_ref","status","timestamp","data"],"properties":{"event":{"type":"string","const":"job.completed"},"jobId":{"type":"string","format":"uuid"},"external_ref":{"type":["string","null"]},"status":{"type":"string","const":"completed"},"operation":{"type":"string","enum":["variation","edit_image","upscale","audiobook_cover"],"description":"Present ONLY for cover-operation jobs. `generate` job webhooks omit this field."},"timestamp":{"type":"string","format":"date-time"},"data":{"oneOf":[{"$ref":"#/components/schemas/GenerateJobResult"},{"$ref":"#/components/schemas/CoverOperationJobResult"},{"$ref":"#/components/schemas/AssetJobResult"}],"description":"The job's result payload with internal `_storagePath` fields stripped. Note: `covers[].signedUrl` in the webhook is the URL signed at completion time (1-hour expiry); poll GET /api/v1/jobs/{jobId} for a fresh URL."}}},"WebhookJobFailed":{"type":"object","required":["event","jobId","external_ref","status","timestamp","error"],"properties":{"event":{"type":"string","const":"job.failed"},"jobId":{"type":"string","format":"uuid"},"external_ref":{"type":["string","null"]},"status":{"type":"string","const":"failed"},"operation":{"type":"string","enum":["variation","edit_image","upscale","audiobook_cover"],"description":"Present ONLY for cover-operation jobs. `generate` job webhooks omit this field."},"timestamp":{"type":"string","format":"date-time"},"error":{"type":"string","description":"Failure message."}}},"BookSummary":{"type":"object","required":["bookId","title","genre","created_at"],"properties":{"bookId":{"type":"string","format":"uuid"},"title":{"type":"string"},"author":{"type":["string","null"]},"subtitle":{"type":["string","null"]},"genre":{"type":"string","description":"Canonical genre value (e.g. `Science Fiction`)."},"subgenre":{"type":["string","null"]},"summary":{"type":["string","null"]},"target_audience":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"BookList":{"type":"object","required":["books","next_cursor"],"properties":{"books":{"type":"array","items":{"$ref":"#/components/schemas/BookSummary"},"description":"Newest first."},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` to fetch the next page. `null` when there are no more results."}}},"BookCoverSummary":{"type":"object","required":["coverId","created_at","assets"],"properties":{"coverId":{"type":"string","format":"uuid","description":"The cover's public id — use with `GET /api/v1/covers/{coverId}` and the download/variation/edit/upscale/audiobook endpoints."},"coverStyle":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"assets":{"type":"object","description":"Per-asset availability flags (no signed URLs — fetch those via `GET /api/v1/covers/{coverId}` or the download endpoint).","properties":{"preview":{"$ref":"#/components/schemas/AssetFlag"},"full":{"$ref":"#/components/schemas/AssetFlag"},"audiobook_preview":{"$ref":"#/components/schemas/AssetFlag"},"audiobook_full":{"$ref":"#/components/schemas/AssetFlag"},"no_text":{"$ref":"#/components/schemas/AssetFlag"},"no_text_full":{"$ref":"#/components/schemas/AssetFlag"}}}}},"UploadTicket":{"type":"object","required":["upload_id","upload_url","expires_at","max_bytes","allowed_types"],"properties":{"upload_id":{"type":"string","description":"Opaque id for the staged upload. Pass it in reference_upload_ids."},"upload_url":{"type":"string","format":"uri","description":"Presigned URL to PUT the image bytes to, using the declared Content-Type."},"expires_at":{"type":"string","format":"date-time","description":"When upload_url stops working (5 minutes after minting)."},"max_bytes":{"type":"integer","description":"Maximum accepted image size in bytes."},"allowed_types":{"type":"array","items":{"type":"string"},"description":"Accepted MIME types."}}},"AssetFlag":{"type":"object","required":["available"],"properties":{"available":{"type":"boolean"}}},"BookCoverList":{"type":"object","required":["bookId","covers","total"],"properties":{"bookId":{"type":"string","format":"uuid"},"covers":{"type":"array","items":{"$ref":"#/components/schemas/BookCoverSummary"},"description":"Newest first."},"total":{"type":"integer"}}},"Health":{"type":"object","required":["status","service","version"],"properties":{"status":{"type":"string","enum":["ok"],"description":"Always `ok` when the API responds. Unreachability is signalled by the transport (connection failure or a 5xx), not by this field."},"service":{"type":"string","description":"Service name, matching `info.title` in the OpenAPI spec."},"version":{"type":"string","description":"API contract version, matching `info.version` in the OpenAPI spec."}}},"CreditSummary":{"type":"object","required":["tier","credits_remaining","credits_monthly","overage_credits","total_available","concurrent_limit","is_active","billing_url"],"properties":{"tier":{"type":"string","description":"Plan tier (e.g. `starter`, `growth`, `scale`)."},"credits_remaining":{"type":"integer","description":"Unused monthly credits."},"credits_monthly":{"type":"integer","description":"Monthly allowance at renewal."},"overage_credits":{"type":"integer","description":"Purchased overage credits."},"total_available":{"type":"integer","description":"credits_remaining + overage_credits."},"concurrent_limit":{"type":"integer","description":"Max queued/processing jobs at once."},"is_active":{"type":"boolean"},"billing_url":{"type":"string","format":"uri","description":"Where a human can manage the plan / top up."}}},"JobSummary":{"type":"object","required":["jobId","status","operation","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"operation":{"type":"string","enum":["generate","variation","edit_image","upscale","audiobook_cover"]},"external_ref":{"type":["string","null"]},"credits_used":{"type":["integer","null"]},"bookId":{"type":["string","null"],"format":"uuid"},"error":{"type":["string","null"],"description":"Failure message when status is `failed`."},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"poll_url":{"type":"string","description":"Path of the job detail endpoint (result payloads with signed URLs live there, not in this list)."}}},"JobList":{"type":"object","required":["jobs","next_cursor"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/JobSummary"},"description":"Newest first. Result payloads are omitted — poll `GET /api/v1/jobs/{jobId}` for signed cover URLs."},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` to fetch the next page. `null` when there are no more results."}}},"CapabilityGenre":{"type":"object","required":["genre","slug","supported","subgenre_auto_detected","subgenres"],"properties":{"genre":{"type":"string","description":"Canonical genre value."},"slug":{"type":"string","description":"API genre slug (accepted by POST /api/v1/books)."},"supported":{"type":"boolean","description":"Whether cover generation works for this genre at all. Unsupported genres return `422 unsupported_genre` from generate."},"subgenre_auto_detected":{"type":"boolean","description":"When true, POST /api/v1/books auto-detects a subgenre from the book details if the caller omits one."},"subgenres":{"type":"array","items":{"type":"object","required":["value","label","supported"],"properties":{"value":{"type":"string"},"label":{"type":"string"},"supported":{"type":"boolean"}}}}}},"Capabilities":{"type":"object","required":["api_version","generation_engine","genres","operations","limits","coverage_note"],"properties":{"api_version":{"type":"string"},"generation_engine":{"type":"string"},"genres":{"type":"array","items":{"$ref":"#/components/schemas/CapabilityGenre"},"description":"Every canonical genre with honest per-subgenre generation coverage, derived from the same routing modules the generate endpoint gates on."},"operations":{"type":"object","description":"Static per-operation credit costs, keyed by job operation.","additionalProperties":{"type":"object","required":["credits_per_image","description"],"properties":{"credits_per_image":{"type":"integer"},"description":{"type":"string"}}}},"limits":{"type":"object","properties":{"max_covers_per_generate":{"type":"integer"}}},"coverage_note":{"type":"string"}}},"WrapDimensions":{"type":"object","description":"Print geometry at 300 DPI. All inch values are the finished canvas including bleed.","properties":{"spineWidth":{"type":"number","description":"Spine width in inches, derived from pageCount and paperType."},"fullWidth":{"type":"number","description":"Full wrap width in inches (back + spine + front + bleed)."},"fullHeight":{"type":"number","description":"Full wrap height in inches (trim height + bleed)."},"pixelWidth":{"type":"integer","description":"fullWidth x dpi, rounded."},"pixelHeight":{"type":"integer","description":"fullHeight x dpi, rounded."},"dpi":{"type":"integer","description":"Always 300 for KDP."},"bleed":{"type":"number","description":"Bleed in inches per edge (0 for hardcover printed case)."},"safeTextMargin":{"type":"number","description":"Minimum inches between text and the trim edge."},"minFontSize":{"type":"number","description":"Smallest point size the layout engine will emit."},"canHaveSpineText":{"type":"boolean","description":"False when the page count makes the spine too thin for lettering; spine text is dropped with a warning."},"barcodeZone":{"type":"object","description":"Rectangle in inches, relative to the BACK COVER origin, that the printer covers with the barcode. The layout engine keeps text out of it.","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"}}},"sections":{"type":"object","description":"Panel boundaries in inches from the left edge of the full wrap.","additionalProperties":{"type":"number"}},"bindingType":{"type":"string","enum":["paperback","hardcover"]}}},"WrapWarning":{"type":"object","description":"A layout adjustment the engine made, or a limitation of the finished wrap. Warnings never fail a job — they tell you what a human at the editor would have seen and fixed.","required":["code","message"],"properties":{"code":{"type":"string","enum":["text_too_long_for_band","text_too_long_for_block","preset_not_applied","text_overflows_panel","clamped_to_safe_area","moved_clear_of_barcode","barcode_zone_unavoidable","font_size_raised_to_minimum","spine_too_thin_for_text","italic_not_available","bold_not_available","image_block_not_rendered","front_outpaint_trimmed","front_outpaint_incomplete"]},"blockId":{"type":"string","description":"The text or image block the warning applies to, when it is block-specific."},"message":{"type":"string"}}},"WrapAsset":{"type":"object","required":["signedUrl","expiresAt"],"properties":{"signedUrl":{"type":"string","format":"uri"},"expiresAt":{"type":"string","format":"date-time"}}},"WrapAssets":{"type":"object","description":"Renditions of one wrap design. Re-signed on every poll/read.","properties":{"wrap_preview":{"allOf":[{"$ref":"#/components/schemas/WrapAsset"}],"description":"1024px-wide JPEG for on-screen review."},"wrap_flat":{"allOf":[{"$ref":"#/components/schemas/WrapAsset"}],"description":"Full-resolution composited JPEG at 300 DPI."},"wrap_pdf":{"allOf":[{"$ref":"#/components/schemas/WrapAsset"}],"description":"Print-ready PDF sized to fullWidth x fullHeight inches — the file you upload to KDP."},"back_art":{"allOf":[{"$ref":"#/components/schemas/WrapAsset"}],"description":"The generated back-cover artwork alone, without text. Present on GET /api/v1/wraps/{wrapId}."}}},"WrapLayoutReport":{"type":"object","description":"What the compositor actually drew, after auto-fit and guardrails.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"dpi":{"type":"integer"},"blocks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fontSize":{"type":"number","description":"Point size after auto-fit, which may be smaller than requested."},"heightPx":{"type":"integer"},"overflowsPanel":{"type":"boolean"}}}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/WrapWarning"}}}},"WrapJobQueued":{"type":"object","required":["jobId","status","operation","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"operation":{"type":"string","enum":["wrap"]},"credits_reserved":{"type":"integer","description":"2 for a solid back, 4 for AI artwork. Refunded in full if the job fails."},"external_ref":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"poll_url":{"type":"string"}}},"WrapExtendJobQueued":{"type":"object","required":["jobId","status","operation","created_at","poll_url"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"operation":{"type":"string","enum":["wrap_extend_front"]},"credits_reserved":{"type":"integer","enum":[0],"description":"Always 0 — re-extending the front of a paid wrap is free."},"external_ref":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"poll_url":{"type":"string"}}},"WrapJobResult":{"type":"object","description":"Result payload for a `wrap` or `wrap_extend_front` job. Unlike cover operations this carries an `assets` map, not a `covers` array — a wrap is several renditions of ONE design. For `wrap_extend_front`, `designStyle` and `preset` are omitted (the wrap's stored layout is reused).","required":["wrapId","assets"],"properties":{"wrapId":{"type":"string","format":"uuid","description":"Pass to GET /api/v1/wraps/{wrapId} and the render endpoint."},"bookId":{"type":"string","format":"uuid"},"coverId":{"type":"string","format":"uuid","description":"Public id of the cover used as the front panel."},"designStyle":{"type":"string","enum":["solid","textured","continuation","complementary"]},"preset":{"type":"string","enum":["standard","blurb-only","minimal"]},"dims":{"$ref":"#/components/schemas/WrapDimensions"},"layout_report":{"$ref":"#/components/schemas/WrapLayoutReport"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/WrapWarning"}},"assets":{"$ref":"#/components/schemas/WrapAssets"}}},"WrapDetail":{"type":"object","description":"A stored wrap design. `layout_state` is the same schema the browser wrap editor reads and writes, so a wrap created through the API can be opened and adjusted by hand.","required":["wrapId","assets"],"properties":{"wrapId":{"type":"string","format":"uuid"},"bookId":{"type":"string","format":"uuid"},"designName":{"type":["string","null"],"description":"`API` for wraps created through this endpoint."},"designStyle":{"type":["string","null"]},"printProvider":{"type":"string"},"bindingType":{"type":["string","null"]},"trimWidth":{"type":"number"},"trimHeight":{"type":"number"},"pageCount":{"type":["integer","null"]},"paperType":{"type":["string","null"]},"spineWidth":{"type":"number"},"fullWidthInches":{"type":"number"},"fullHeightInches":{"type":"number"},"layout_state":{"type":["object","null"],"description":"Editor-schema layout: text blocks in percent-of-panel coordinates, spine styles, overlay settings."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"assets":{"$ref":"#/components/schemas/WrapAssets"}}},"WrapRenderResult":{"type":"object","required":["wrapId","assets"],"properties":{"wrapId":{"type":"string","format":"uuid"},"bookId":{"type":"string","format":"uuid"},"dims":{"$ref":"#/components/schemas/WrapDimensions"},"layout_report":{"$ref":"#/components/schemas/WrapLayoutReport"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/WrapWarning"}},"assets":{"$ref":"#/components/schemas/WrapAssets"}}}}}}