{"openapi":"3.1.0","info":{"title":"Neurolate External API","description":"Every request needs a team API key: `Authorization: Bearer nlk_…`. A team owner creates keys\non the website; the full key is shown once.\n\n- **Operations** — `/slicing`, `/ocr`, `/clean`, `/styles`: send images, get the result in the\n  same response.\n- **Chapters** — `/projects`, `/chapters`: upload a chapter into a team project and process it\n  in the background. The results stay on the website; this API does not return them.\n\n## Sending images\n\n- `multipart/form-data`: one `images` part per image, in order; PNG or JPEG, up to 50 MB each,\n  500 MB per request, any number. Other fields are text parts of the same form.\n- **Chapters file by file** — for large chapters or an unstable connection: `POST /ext/v1/uploads`\n  gives an upload URL per file, `PUT` each file there (a new connection per file; a file that\n  crawls — abort and send again), then `POST /ext/v1/chapters/from-uploads` with the `uploadId`s.\n  A cut-off connection then costs one file, not the whole chapter.\n- Coordinates are pixels of your image, origin at the top left.\n- **Masks** — `masks` in OCR, Clean and Styles: a JSON array sent as text, e.g.\n  `[{\"image\":0,\"bbox\":{\"x\":10,\"y\":20,\"width\":300,\"height\":120}},{\"image\":1,\"polygon\":[[0,0],[50,0],[50,40]]}]`.\n  `image` (required) — zero-based index of the `images` part; each area has either `bbox`\n  (integers) or `polygon` — one ring of 3+ `[x, y]` points, fractions allowed, no holes. Areas\n  must lie inside the image, edges included (`x = width` is fine).\n  To send back a mask the API returned, use each of its polygons' `outer` as a mask.\n- **Block or page** — `kind` in OCR, Clean and Styles: BLOCK is one cropped speech bubble, PAGE\n  is a whole page. Omitted — decided per image: both sides up to 690 px is a BLOCK, otherwise a\n  PAGE; send `kind=PAGE` for smaller pages. An explicit BLOCK may be up to 1,000,000 px\n  (width × height).\n- Result images (`responseFormat`): `url` — a link valid for 1 hour, on another host; download\n  it **without** the `Authorization` header. `base64` — a `data:image/png;base64,…` string.\n\n## Time\n\nOCR, Clean and Styles finish within about 4.5 minutes; set your client timeout to at least 5. A\npart that did not finish in time comes back with error `DEADLINE`, not charged — send it again.\n\n## Errors\n\n`{\"code\": \"...\", \"message\": \"...\"}`, plus the zero-based index of the bad image or area when\nthere is one (`index`). Each operation lists its codes.\nAny endpoint may answer:\n\n- `401` API_KEY_MISSING / API_KEY_INVALID — no key, a wrong or a revoked one;\n- `403` API_KEY_DISABLED / API_KEY_EXPIRED;\n- `413` — an image over 50 MB or a request over 500 MB.\n\nWhen one image, block or area fails on our side, the request still succeeds: that item has an\n`error` (`{code, message}`) and is not charged. `502` — everything failed.\n\n## Billing\n\nPaid operations spend tokens (6 decimal places) from the team balance, within the key's\nspending limit. Failures on our side are never charged.\n\n- **Slicing** costs are known up front: charged before the work, refunded if we fail.\n- **OCR, Clean, Styles** — how many blocks there are is known only after the work. Before it we\n  charge a **hold**: the price of the blocks in the request — your masks, one per BLOCK image —\n  but at most 50; a PAGE without masks counts as 50. Ranged rates (`api_ocr_text`,\n  `api_clean_premium`) are held at their upper bound. After the work the hold is replaced by\n  the actual cost: what is left over comes back at once; if the work cost more, the rest is\n  charged as far as the balance and the key limit allow. Every request holds its own, so\n  parallel requests each need theirs. If everything failed on our side, the hold comes back.\n- Not enough for the price or the hold: `402` INSUFFICIENT_BALANCE (team balance) or\n  API_KEY_LIMIT_EXCEEDED (key limit; free endpoints keep working), and nothing is done.\n- Each operation returns `usage`: `cost` and lines \"price item × quantity\".\n- `GET /ext/v1/prices` — your team's rates; `GET /ext/v1/prices/default` — the platform's\n  default rates, no key needed; `GET /ext/v1/key` — what is left of the key limit.\n- No rate limits: the key limit and the team balance are the only caps.\n\n## Retries\n\nA POST sent twice is done and charged twice — unless it carries an `Idempotency-Key` header (any\nstring up to 255 characters, e.g. a UUID). A second POST with a key the team has already used\nis not done and not charged: it answers `409 DUPLICATE_REQUEST` with `requestId` of the first\none, whose outcome and cost are in `GET /ext/v1/requests/{requestId}` (`httpStatus` null — still\nrunning). This holds even when the first one failed: to try again, use a new key. Keys are\nremembered as long as the request log, 90 days. `POST /ext/v1/uploads` is free and not logged:\nit takes no key, just call it again.\n\n## Request log\n\nEvery request has an id: the `X-Request-Id` header and `requestId` in the response.\n`GET /ext/v1/requests/{id}` — its outcome and cost, kept for 90 days. Chapter OCR is charged\nas it runs, so its cost appears there, not in the upload response.\n\nThis specification: `GET /ext/v1/openapi.json`, `GET /ext/v1/openapi.yaml`. Both, and the default\nrates, need no key.","version":"v1"},"servers":[{"url":"https://api.neurolate.com"}],"security":[{"apiKey":[]}],"tags":[{"name":"Key","description":"Check the key and its remaining limit"},{"name":"Prices","description":"Your team's rates and the token price"},{"name":"Slicing","description":"Split a webtoon strip into pages"},{"name":"OCR","description":"Text block detection: masks and recognition"},{"name":"Clean","description":"Remove text from images"},{"name":"Styles","description":"Text color and effects of text blocks"},{"name":"Projects","description":"Your team's projects"},{"name":"Uploads","description":"Upload images file by file before a chapter upload"},{"name":"Chapters","description":"Upload chapters and run slicing + OCR on them"},{"name":"Request log","description":"Outcome and cost of past requests"}],"paths":{"/ext/v1/uploads":{"post":{"tags":["Uploads"],"summary":"Get upload URLs","description":"One upload URL per file, in the order of `files`. Then send each file with `PUT uploadUrl`, the body is the file itself, exactly `size` bytes (the size is part of the signature). Files can go in any order and in parallel. Then pass the `uploadId`s to `POST /ext/v1/chapters/from-uploads`.\n\nA file that is cut off or crawls costs only that file: abort it and send it again over a new connection, to the same URL while it is valid. If `uploadUrl` cannot be reached at all (its port is closed in your network), use `fallbackUploadUrl`.\n\nURLs are valid for 1 hour; files not used by an operation are deleted after 3 hours — for files that did not make it in time, ask for new URLs. Free.","operationId":"createUploads","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtUploadsRequest"}}},"required":true},"responses":{"201":{"description":"Upload URLs, in the order of `files`.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtUploadSlotDTO"}}}}},"400":{"description":"NO_FILES, TOO_MANY_FILES, EMPTY_FILE (`index`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtUploadErrorDTO"}}}},"413":{"description":"FILE_TOO_LARGE (`index`) — a file over 50 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtUploadErrorDTO"}}}}}}},"/ext/v1/styles":{"post":{"tags":["Styles"],"summary":"Find text color and effects","description":"Main text color and effects (stroke, glow, shadow, gradient) of every text block. **Color webtoons only**: on black-and-white manga the results are unreliable, and still charged.\n\nBlocks — your `masks`; for an image without masks we find them: on a BLOCK — the text inside it, on a PAGE — OCR blocks where text was read.\n\nBilling per block without `error`: `api_styles`. A block we found adds `api_ocr_mask`: 1 on a BLOCK image, 2 on a PAGE. Before the work a hold is charged, after it the actual cost (see Billing).","operationId":"findStyles","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}},"masks":{"type":"string","description":"Text blocks, JSON text (see the overview)."},"language":{"$ref":"#/components/schemas/Language","description":"Source language, not UND. Required when some image has no masks."},"kind":{"type":"string","description":"Applies to every image; default — by size (see the overview).","enum":["BLOCK","PAGE"]}},"required":["images"]}}}},"responses":{"200":{"description":"Blocks of every image with their color and effects.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StylesResponseDTO"}}}},"400":{"description":"Invalid fields, masks or image (`index`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtStylesErrorDTO"}}}},"402":{"description":"INSUFFICIENT_BALANCE / API_KEY_LIMIT_EXCEEDED — the balance or the key limit can't cover the hold of this request (see Billing); nothing was done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtStylesErrorDTO"}}}},"409":{"description":"DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtRequestErrorDTO"}}}},"413":{"description":"TOTAL_TOO_LARGE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtStylesErrorDTO"}}}},"502":{"description":"WORKER_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtStylesErrorDTO"}}}}}}},"/ext/v1/slicing":{"post":{"tags":["Slicing"],"summary":"Slice a strip into pages","description":"`images` — consecutive pieces of one webtoon strip, top to bottom. Returns where to cut it into about `pages` pages. A page is at most 36.25 × `width` px tall (25,012 px at 690), so there may be more.\n\nCharged per 10,000 px of strip height at 690 px width: `api_slicing_find` without `cut`; with `cut=true` you also get the PNG pages, charged `api_slicing_cut` instead. Charged up front, refunded if we fail.","operationId":"slice","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}},"pages":{"type":"integer","description":"Target number of pages.","maximum":500,"minimum":1},"cut":{"type":"boolean","default":false,"description":"Also return the cut pages (paid)."},"width":{"type":"integer","description":"Strip width the images are scaled to. Default — the most common image width.","maximum":4000,"minimum":100},"responseFormat":{"type":"string","default":"url","enum":["url","base64"]}},"required":["images","pages"]}}}},"responses":{"200":{"description":"Cuts, and pages when `cut=true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlicingResponseDTO"}}}},"400":{"description":"Invalid fields or image (`index`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtSlicingErrorDTO"}}}},"402":{"description":"`cut=true` only.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtSlicingErrorDTO"}}}},"409":{"description":"DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtRequestErrorDTO"}}}},"413":{"description":"TOTAL_TOO_LARGE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtSlicingErrorDTO"}}}},"502":{"description":"WORKER_FAILED; the charge is refunded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtSlicingErrorDTO"}}}}}}},"/ext/v1/ocr":{"post":{"tags":["OCR"],"summary":"Detect text blocks: masks and/or text","description":"Finds text blocks and returns their masks and/or text, in reading order.\n\n- PAGE — we find the blocks. TEXT on a page runs as MASK_AND_TEXT (see `modeApplied`).\n- BLOCK — the whole image is one block; we find the text shape inside it.\n- With `masks` — one block per mask, nothing outside them; images without masks are skipped.\n\nPages and BLOCK images: blocks without text are dropped and free (MASK mode reads no text and returns every mask). Your mask always comes back and is charged, even with no text (`textStatus` EMPTY).\n\nBilling per returned block: mask — `api_ocr_mask` (not in TEXT mode), text — `api_ocr_text` (not in MASK mode). Before the work a hold is charged, after it the actual cost (see Billing).","operationId":"recognize","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}},"mode":{"type":"string","default":"MASK_AND_TEXT","enum":["MASK_AND_TEXT","MASK","TEXT"]},"language":{"$ref":"#/components/schemas/Language","description":"Source language, not UND."},"type":{"type":"string","description":"Sets the reading order of blocks.","enum":["MANGA","WEBTOON"]},"readingDirection":{"type":"string","description":"Default — RTL for `language=JA` with `type=MANGA`, LTR otherwise.","enum":["LTR","RTL"]},"masks":{"type":"string","description":"Masks to read, JSON text (see the overview)."},"kind":{"type":"string","description":"Applies to every image; default — by size (see the overview).","enum":["BLOCK","PAGE"]}},"required":["images","language","type"]}}}},"responses":{"200":{"description":"Blocks of every image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OcrResponseDTO"}}}},"400":{"description":"Invalid fields, masks or image (`index`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtOcrErrorDTO"}}}},"402":{"description":"INSUFFICIENT_BALANCE / API_KEY_LIMIT_EXCEEDED — the balance or the key limit can't cover the hold of this request (see Billing); nothing was done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtOcrErrorDTO"}}}},"409":{"description":"DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtRequestErrorDTO"}}}},"413":{"description":"TOTAL_TOO_LARGE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtOcrErrorDTO"}}}},"502":{"description":"WORKER_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtOcrErrorDTO"}}}}}}},"/ext/v1/clean":{"post":{"tags":["Clean"],"summary":"Clean text off images","description":"Removes text. Each image with something cleaned comes back whole, as a PNG of the same size; `blocks` lists its areas.\n\nAreas — your `masks`; for an image without masks we find them: on a BLOCK — the text inside it, on a PAGE — OCR blocks where text was read.\n\n- `tier=PREMIUM`, `mode=MASK` — a premium model per area.\n- `tier=PREMIUM`, `mode=BBOX` — redraws the whole bounding box with a stronger model; for text over a face or a very complex background. Its model cost is higher, so it lands nearer the top of the `api_clean_premium` range.\n\nBilling per cleaned area: STANDARD — `api_clean`, PREMIUM — `api_clean_premium`. An area we found adds `api_ocr_mask`: 1 on a BLOCK image, 2 on a PAGE. Before the work a hold is charged, after it the actual cost (see Billing).","operationId":"clean","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}},"masks":{"type":"string","description":"Areas to clean, JSON text (see the overview)."},"tier":{"type":"string","default":"STANDARD","enum":["STANDARD","PREMIUM"]},"mode":{"type":"string","default":"MASK","description":"MASK — erase the mask shape; BBOX — erase its bounding box, with the model of the chosen tier. BBOX is for BLOCK images only; on a page send rectangle masks with MASK.","enum":["MASK","BBOX"]},"type":{"type":"string","description":"Picks the cleaning model.","enum":["MANGA","WEBTOON"]},"language":{"$ref":"#/components/schemas/Language","description":"Source language, not UND. Required when some image has no masks."},"readingDirection":{"type":"string","description":"Used only to find masks. Default — RTL for `language=JA` with `type=MANGA`, LTR otherwise.","enum":["LTR","RTL"]},"responseFormat":{"type":"string","default":"url","enum":["url","base64"]},"kind":{"type":"string","description":"Applies to every image; default — by size (see the overview).","enum":["BLOCK","PAGE"]}},"required":["images","type"]}}}},"responses":{"200":{"description":"A result for every image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleanResponseDTO"}}}},"400":{"description":"Invalid fields, masks or image (`index`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtCleanErrorDTO"}}}},"402":{"description":"INSUFFICIENT_BALANCE / API_KEY_LIMIT_EXCEEDED — the balance or the key limit can't cover the hold of this request (see Billing); nothing was done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtCleanErrorDTO"}}}},"409":{"description":"DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtRequestErrorDTO"}}}},"413":{"description":"TOTAL_TOO_LARGE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtCleanErrorDTO"}}}},"502":{"description":"WORKER_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtCleanErrorDTO"}}}}}}},"/ext/v1/chapters":{"get":{"tags":["Chapters"],"summary":"List chapters of a project","description":"Sorted by chapter number. Free.","operationId":"listChapters","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archived","in":"query","description":"true — archived chapters instead of active ones.","required":false,"schema":{"type":"boolean","default":false}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":30,"maximum":100,"minimum":1}}],"responses":{"200":{"description":"A page of chapters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterPageDTO"}}}},"400":{"description":"INVALID_PAGINATION — page < 0 or size outside 1..100.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"404":{"description":"PROJECT_NOT_FOUND — no such project in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}}}},"post":{"tags":["Chapters"],"summary":"Upload a chapter","description":"Creates a chapter from source images (in reading order, up to 500) and optionally starts processing: `assemble` and `ocr`, as in `POST /ext/v1/chapters/{id}/pipeline`. Processing runs in the background — poll `GET /ext/v1/chapters/{id}/pipeline`. Manga images must be single pages or two-page spreads: height ≤ 2.5 × width and width ≤ 2 × height.\n\nUpload and assembling are free. If processing could not start (e.g. 402), the chapter is still created: 201 with `pipelineError`. A chapter never assembled is deleted after 2 days.","operationId":"createChapter","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}},"projectId":{"type":"string","format":"uuid"},"number":{"type":"number","description":"Chapter number, ≥ 0, may be fractional (e.g. 12.5); unique among the project's active chapters."},"readingDirection":{"type":"string","description":"Default — the project's.","enum":["LTR","RTL"]},"assemble":{"type":"boolean","default":false,"description":"Build pages from the images: a webtoon strip is sliced into about `pages` pages, a manga image is one page."},"width":{"type":"integer","description":"Page width: webtoon 600–800 (default 690), manga 600–1500 (default — the typical image width).","maximum":1500,"minimum":600},"pages":{"type":"integer","description":"Webtoon only, required with `assemble`: about how many pages to slice into.","maximum":500,"minimum":1},"ocr":{"type":"boolean","default":false,"description":"Find and read text on every page not read yet. Needs `assemble` until the chapter is assembled."},"language":{"$ref":"#/components/schemas/Language","description":"Source language for `ocr`, not UND. Default — the project's (required if the project has none); a passed value is saved to the project."}},"required":["images","number","projectId"]}}}},"responses":{"201":{"description":"Chapter created; `pipeline` — the started processing, or `pipelineError` — why it did not start.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterCreatedDTO"}}}},"400":{"description":"Invalid fields or image (`index`); nothing was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"404":{"description":"PROJECT_NOT_FOUND — no such project in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"409":{"description":"NUMBER_TAKEN — the project already has an active chapter with this number; DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ExtChapterErrorDTO"},{"$ref":"#/components/schemas/ExtRequestErrorDTO"}]}}}},"413":{"description":"TOTAL_TOO_LARGE — images exceed 500 MB in total.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}}}}},"/ext/v1/chapters/{id}/pipeline":{"get":{"tags":["Chapters"],"summary":"Chapter processing status","description":"The running or the last finished processing of the chapter (also one started on the website). Poll every few seconds until `status` is DONE or FAILED. Free.","operationId":"getPipeline","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtPipelineDTO"}}}},"404":{"description":"CHAPTER_NOT_FOUND; NO_PIPELINE — the chapter was never processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}}}},"post":{"tags":["Chapters"],"summary":"Process a chapter","description":"Starts `assemble` and/or `ocr` on an uploaded chapter. On an assembled chapter `assemble` is skipped and OCR reads only pages not read yet, so it is safe to repeat. Runs in the background — poll `GET /ext/v1/chapters/{id}/pipeline`.\n\nOCR is charged once per chapter, for the whole chapter, at the team's chapter rate (`chapter_processing` for a webtoon, `chapter_processing_page` for manga); repeat runs are free, and so is a chapter whose OCR was already paid on the website. 402 means the balance or key limit can't cover that price. The charge happens while processing runs and shows in `GET /ext/v1/requests/{id}` of the request that started it; it is final once the processing is DONE or FAILED.","operationId":"startPipeline","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtPipelineRequest"}}},"required":true},"responses":{"202":{"description":"Processing started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtPipelineDTO"}}}},"400":{"description":"NO_STEPS, invalid fields, ASSEMBLE_REQUIRED, LANGUAGE_REQUIRED, or ASSEMBLE_REJECTED (`detail` — why the images cannot be assembled at this width).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"402":{"description":"INSUFFICIENT_BALANCE / API_KEY_LIMIT_EXCEEDED — not enough for the OCR of this chapter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"404":{"description":"CHAPTER_NOT_FOUND — no such chapter in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"409":{"description":"PIPELINE_RUNNING — the chapter is already being processed; ALREADY_ASSEMBLED — only `assemble` was asked for an assembled chapter; NO_IMAGES — the chapter has no source images; DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ExtChapterErrorDTO"},{"$ref":"#/components/schemas/ExtRequestErrorDTO"}]}}}}}}},"/ext/v1/chapters/from-uploads":{"post":{"tags":["Chapters"],"summary":"Create a chapter from uploaded images","description":"The same as `POST /ext/v1/chapters`, but the images are already uploaded file by file with `POST /ext/v1/uploads`; `uploadIds` lists them in reading order. Recommended for large chapters and slow or unstable connections: a cut-off file is sent again on its own, and this request is small.\n\nSafe to repeat with a new `Idempotency-Key` or none (the same key answers DUPLICATE_REQUEST): when these uploads already became this chapter (same project and number, exactly these images), it answers 200 with that chapter and starts nothing again — check `pipeline`, or use `POST /ext/v1/chapters/{id}/pipeline`. NUMBER_TAKEN right after a timed-out attempt may be that attempt still running: wait a few seconds and repeat. Upload and assembling are free.","operationId":"createChapterFromUploads","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 characters, e.g. a UUID. A repeat with the same key is not done: 409 DUPLICATE_REQUEST.","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterFromUploadsRequest"}}},"required":true},"responses":{"201":{"description":"Chapter created; `pipeline` — the started processing, or `pipelineError` — why it did not start.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterCreatedDTO"}}}},"200":{"description":"Repeat: the chapter was already created from these uploads; `pipeline` — its latest processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterCreatedDTO"}}}},"400":{"description":"Invalid fields, INVALID_UPLOAD_ID / DUPLICATE_UPLOAD (`index`), or an invalid image (`index`); nothing was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"404":{"description":"PROJECT_NOT_FOUND — no such project in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}},"409":{"description":"NOT_UPLOADED (`index`) — that file is not there yet. A PUT from outside Russia may be accepted by a relay that delivers it to storage a few minutes later: repeat this request (a new `Idempotency-Key` or none) every 10–30 s for up to 10 minutes, and only then upload that file again (new URLs if they expired). NUMBER_TAKEN; UPLOADS_USED — these uploads already became another chapter; DUPLICATE_REQUEST — this `Idempotency-Key` was already used.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ExtChapterErrorDTO"},{"$ref":"#/components/schemas/ExtRequestErrorDTO"}]}}}},"413":{"description":"TOTAL_TOO_LARGE — images exceed 500 MB in total.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}}}}},"/ext/v1/requests/{id}":{"get":{"tags":["Request log"],"summary":"Get a logged request","description":"Outcome, input summary and cost (usage). The log is kept for 90 days. Free.\n\n**Access:** Requests made with any key of the same team are visible.","operationId":"getRequest","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiRequestDTO"}}}},"404":{"description":"REQUEST_NOT_FOUND — no such request in the log of the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtRequestErrorDTO"}}}}}}},"/ext/v1/projects":{"get":{"tags":["Projects"],"summary":"List projects","description":"Projects of the key's team, newest first. Projects are created on the website. Free.","operationId":"listProjects","parameters":[{"name":"search","in":"query","description":"Substring of the name or the original name, case-insensitive.","required":false,"schema":{"type":"string"}},{"name":"archived","in":"query","description":"true — only archived, false — only active; omitted — both.","required":false,"schema":{"type":"boolean"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":30,"maximum":100,"minimum":1}}],"responses":{"200":{"description":"A page of projects.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtProjectPageDTO"}}}},"400":{"description":"INVALID_PAGINATION — page < 0 or size outside 1..100.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtProjectErrorDTO"}}}}}}},"/ext/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","description":"Free.","operationId":"getProject","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtProjectDTO"}}}},"404":{"description":"PROJECT_NOT_FOUND — no such project in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtProjectErrorDTO"}}}}}}},"/ext/v1/prices":{"get":{"tags":["Prices"],"summary":"Rates and token price","description":"The team's rates for everything a key can be charged for, in tokens, and the token price in each currency the website shows. A rate may change at any time; what you were actually charged is in `usage` and in the request log. Free.","operationId":"getPrices","responses":{"200":{"description":"Rates and token price.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtPricesDTO"}}}}}}},"/ext/v1/prices/default":{"get":{"tags":["Prices"],"summary":"Default rates","description":"The platform's default rates for everything a key can be charged for, and the token price in each currency. No key needed. A team may have its own rates — `GET /ext/v1/prices` shows yours. A rate may change at any time. Free.\n\n**Доступ:** Без токена, роут открыт всем.","operationId":"getDefaultPrices","responses":{"200":{"description":"Default rates and token price.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtPricesDTO"}}}}},"security":[]}},"/ext/v1/key":{"get":{"tags":["Key"],"summary":"Current key","description":"Key name, spending limit and how much of it is used. Free.","operationId":"getCurrentKey","responses":{"200":{"description":"The key this request was made with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtKeyDTO"}}}}}}},"/ext/v1/chapters/{id}":{"get":{"tags":["Chapters"],"summary":"Get a chapter","description":"Status and page counters. Free.","operationId":"getChapter","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The chapter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterDTO"}}}},"404":{"description":"CHAPTER_NOT_FOUND — no such chapter in the key's team.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtChapterErrorDTO"}}}}}}}},"components":{"schemas":{"ExtUploadFile":{"type":"object","properties":{"size":{"type":"integer","format":"int64","description":"Exact file size in bytes, up to 50 MB; the PUT must send exactly this many."}}},"ExtUploadsRequest":{"type":"object","properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/ExtUploadFile"},"maxItems":500,"minItems":1}}},"ExtUploadSlotDTO":{"type":"object","properties":{"uploadId":{"type":"string","format":"uuid","description":"Pass it to the operation once the file is uploaded."},"uploadUrl":{"type":"string","description":"`PUT` the file here; the body is the file itself. Any Content-Type — we detect the format."},"fallbackUploadUrl":{"type":["string","null"],"description":"The same PUT through the main entrance, if `uploadUrl` cannot be reached. null — there is none."},"expiresAt":{"type":"string","format":"date-time","description":"Both URLs stop working at this time."}}},"ExtUploadErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["NO_FILES","TOO_MANY_FILES","EMPTY_FILE","FILE_TOO_LARGE"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"Zero-based index in 'files'."}}},"ApiUsageDTO":{"type":"object","description":"What the request cost. A line = price item × quantity; cost = sum of lines.","properties":{"cost":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiUsageItemDTO"}}}},"ApiUsageItemDTO":{"type":"object","properties":{"item":{"type":["string","null"],"description":"Price item code, e.g. api_slicing_cut."},"name":{"type":["string","null"]},"unit":{"type":["string","null"],"description":"Unit of quantity, as in `GET /ext/v1/prices`.","enum":["BLOCK","NORM_HEIGHT_10K_PX","PAGE"]},"quantity":{"type":["number","null"]},"amount":{"type":"number","description":"Line cost in tokens, 6 decimal places."}}},"Box":{"type":"object","description":"Rectangle in image pixels: top-left corner, width and height.","properties":{"x":{"type":"integer","format":"int32"},"y":{"type":"integer","format":"int32"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"}}},"ItemError":{"type":"object","description":"A failure on our side for one image, block or area; not charged. `code` DEADLINE — not finished in time, send it again; any other code — processing failed.","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"Polygon":{"type":"object","description":"`outer` — [x, y] points in image pixels; `exclude` — holes, each a list of points.","properties":{"outer":{"type":"array","items":{"type":"array","items":{"type":"integer","format":"int32"}}},"exclude":{"type":"array","items":{"type":"array","items":{"type":"array","items":{"type":"integer","format":"int32"}}}}}},"StylesBlockDTO":{"type":"object","properties":{"number":{"type":"integer","format":"int32","description":"1-based position among this image's blocks."},"mask":{"type":["array","null"],"description":"Block in image pixels.","items":{"$ref":"#/components/schemas/Polygon"}},"bbox":{"oneOf":[{"$ref":"#/components/schemas/Box"},{"type":"null"}]},"textColor":{"type":["string","null"],"description":"Main text color, #rrggbb. null — not determined."},"effects":{"type":"array","description":"Effects on the text; empty — plain text.","items":{"$ref":"#/components/schemas/StylesEffectDTO"}},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"StylesEffectDTO":{"type":"object","properties":{"kind":{"type":"string","description":"SHADOW has only a color: its offset and blur are not determined. The text color itself is in `textColor`, never here.","enum":["STROKE","GLOW","SHADOW","GRADIENT"]},"color":{"type":"string","description":"#rrggbb. For GRADIENT — the start color."},"color2":{"type":["string","null"],"description":"GRADIENT only: the end color, #rrggbb."},"direction":{"type":["integer","null"],"format":"int32","description":"GRADIENT only: angle in degrees, 0..359, as in CSS."},"size":{"type":["integer","null"],"format":"int32","description":"STROKE and GLOW: size in pixels of your image, as in Photoshop layer styles — each stroke is measured from the edge of the text (a second stroke includes the first), and so is glow, strokes included. null — not determined."}}},"StylesImageDTO":{"type":"object","properties":{"index":{"type":"integer","format":"int32"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"},"kind":{"type":"string","description":"BLOCK — one cropped bubble, PAGE — a page (see `kind`).","enum":["BLOCK","PAGE"]},"masks":{"type":"string","description":"CLIENT — your masks were used; DETECTED — we found the masks ourselves.","enum":["CLIENT","DETECTED"]},"blocks":{"type":"array","description":"Text blocks: your masks in order, or the ones we found.","items":{"$ref":"#/components/schemas/StylesBlockDTO"}},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"StylesResponseDTO":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid","description":"Request id — the same as the X-Request-Id header."},"images":{"type":"array","items":{"$ref":"#/components/schemas/StylesImageDTO"}},"usage":{"$ref":"#/components/schemas/ApiUsageDTO"}}},"ExtStylesErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["NO_IMAGES","INVALID_LANGUAGE","LANGUAGE_REQUIRED","TOTAL_TOO_LARGE","INVALID_IMAGE","INVALID_KIND","BLOCK_TOO_LARGE","INVALID_MASKS","INSUFFICIENT_BALANCE","API_KEY_LIMIT_EXCEEDED","WORKER_FAILED"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"Zero-based index of the bad image (INVALID_IMAGE, BLOCK_TOO_LARGE) or mask (INVALID_MASKS)."}}},"ExtRequestErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["REQUEST_NOT_FOUND","DUPLICATE_REQUEST","INVALID_IDEMPOTENCY_KEY"]},"message":{"type":"string"},"requestId":{"type":["string","null"],"format":"uuid","description":"DUPLICATE_REQUEST: the first request with this key; its outcome and cost — `GET /ext/v1/requests/{requestId}`."}}},"SlicingCutDTO":{"type":"object","properties":{"fraction":{"type":"number","format":"double","description":"Fraction of the strip height, 0..1."},"stripY":{"type":"integer","format":"int32","description":"The same cut as a row of the strip at `width`: the new page starts at this row."},"image":{"type":"integer","format":"int32","description":"Zero-based index of the source image, in upload order."},"y":{"type":"integer","format":"int32","description":"The same cut in that image's own pixels: the new page starts at this row."}}},"SlicingPageDTO":{"type":"object","properties":{"index":{"type":"integer","format":"int32"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"},"url":{"type":"string","description":"Page PNG: a link valid for 1 hour (responseFormat=url) or data:image/png;base64,… (responseFormat=base64)."},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When the link expires. null for base64."}}},"SlicingResponseDTO":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid","description":"Request id — the same as the X-Request-Id header."},"width":{"type":"integer","format":"int32","description":"Strip width the cuts were computed at (and pages were cut at)."},"stripHeight":{"type":"integer","format":"int32","description":"Strip height at that width."},"cuts":{"type":"array","description":"Cuts from top to bottom; N cuts = N+1 pages.","items":{"$ref":"#/components/schemas/SlicingCutDTO"}},"pages":{"type":["array","null"],"description":"Ready pages — only when cut=true.","items":{"$ref":"#/components/schemas/SlicingPageDTO"}},"usage":{"$ref":"#/components/schemas/ApiUsageDTO"}}},"ExtSlicingErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["NO_IMAGES","INVALID_PAGES","INVALID_WIDTH","INVALID_RESPONSE_FORMAT","TOTAL_TOO_LARGE","INVALID_IMAGE","INSUFFICIENT_BALANCE","API_KEY_LIMIT_EXCEEDED","WORKER_FAILED"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"INVALID_IMAGE: zero-based index of the image."}}},"FuriganaSpan":{"type":"object","properties":{"start":{"type":"integer","format":"int32"},"end":{"type":"integer","format":"int32"},"reading":{"type":"string"}}},"OcrBlockDTO":{"type":"object","properties":{"number":{"type":"integer","format":"int32","description":"1-based, in reading order; for your mask — `maskIndex` + 1."},"maskIndex":{"type":["integer","null"],"format":"int32","description":"Zero-based index in the whole `masks` array, when the block answers your mask."},"text":{"type":["string","null"]},"furigana":{"type":["array","null"],"description":"Kanji readings, Japanese only: a list of `{start, end, reading}` sorted by `start`, non-overlapping. `[start, end)` is a half-open span of `text` in Unicode code points (not UTF-16 units: a character outside the BMP counts as one); `reading` is the kana reading. null — no readings.","items":{"$ref":"#/components/schemas/FuriganaSpan"}},"mask":{"type":["array","null"],"description":"Shape of the text, in image pixels; also inside a BLOCK or a region. null in TEXT mode.","items":{"$ref":"#/components/schemas/Polygon"}},"bbox":{"oneOf":[{"$ref":"#/components/schemas/Box"},{"type":"null"}]},"textStatus":{"type":["string","null"],"description":"Your masks only, not in MASK mode: OK — text found, EMPTY — no text (charged), FAILED — our failure (not charged).","enum":["OK","EMPTY","FAILED"]},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"OcrImageDTO":{"type":"object","properties":{"index":{"type":"integer","format":"int32"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"},"modeApplied":{"type":["string","null"],"description":"The mode actually applied. TEXT without masks on a page switches to MASK_AND_TEXT (and is charged as such); a block (see `kind`) is a crop — one whole block in the requested mode. null — the image was not processed: masks were given, but none for it.","enum":["MASK_AND_TEXT","MASK","TEXT"]},"blocks":{"type":"array","description":"Blocks in reading order. On pages — only blocks where text was found (except in MASK mode).","items":{"$ref":"#/components/schemas/OcrBlockDTO"}},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"OcrResponseDTO":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid","description":"Request id — the same as the X-Request-Id header."},"images":{"type":"array","items":{"$ref":"#/components/schemas/OcrImageDTO"}},"usage":{"$ref":"#/components/schemas/ApiUsageDTO"}}},"ExtOcrErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["NO_IMAGES","INVALID_MODE","INVALID_LANGUAGE","INVALID_TYPE","INVALID_READING_DIRECTION","INVALID_KIND","TOTAL_TOO_LARGE","INVALID_IMAGE","BLOCK_TOO_LARGE","INVALID_MASKS","INSUFFICIENT_BALANCE","API_KEY_LIMIT_EXCEEDED","WORKER_FAILED"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"Zero-based index of the bad image (INVALID_IMAGE, BLOCK_TOO_LARGE) or mask (INVALID_MASKS)."}}},"CleanBlockDTO":{"type":"object","properties":{"number":{"type":"integer","format":"int32","description":"1-based position among this image's areas."},"mask":{"type":["array","null"],"description":"Area in image pixels.","items":{"$ref":"#/components/schemas/Polygon"}},"bbox":{"oneOf":[{"$ref":"#/components/schemas/Box"},{"type":"null"}]},"cleaned":{"type":"boolean","description":"Whether this area is cleaned in `image`. Only cleaned areas are charged."},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"CleanImageDTO":{"type":"object","properties":{"index":{"type":"integer","format":"int32"},"width":{"type":"integer","format":"int32"},"height":{"type":"integer","format":"int32"},"kind":{"type":"string","description":"BLOCK — one cropped bubble, PAGE — a page: as given in `kind`, or by size without it.","enum":["BLOCK","PAGE"]},"masks":{"type":"string","description":"CLIENT — your masks were used; DETECTED — we found the masks ourselves.","enum":["CLIENT","DETECTED"]},"url":{"type":["string","null"],"description":"Cleaned PNG of the whole image: a link valid for 1 hour (responseFormat=url) or data:image/png;base64,… (responseFormat=base64). null — nothing was cleaned: no areas, or every area failed (see `error`)."},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When the link expires. null for base64."},"blocks":{"type":"array","description":"All areas of this image — your masks in order, or the ones we found; `cleaned` tells which were cleaned.","items":{"$ref":"#/components/schemas/CleanBlockDTO"}},"error":{"oneOf":[{"$ref":"#/components/schemas/ItemError"},{"type":"null"}]}}},"CleanResponseDTO":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid","description":"Request id — the same as the X-Request-Id header."},"images":{"type":"array","items":{"$ref":"#/components/schemas/CleanImageDTO"}},"usage":{"$ref":"#/components/schemas/ApiUsageDTO"}}},"ExtCleanErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["NO_IMAGES","INVALID_TIER","INVALID_MODE","INVALID_TYPE","INVALID_LANGUAGE","LANGUAGE_REQUIRED","INVALID_READING_DIRECTION","INVALID_RESPONSE_FORMAT","INVALID_KIND","TOTAL_TOO_LARGE","INVALID_IMAGE","BLOCK_TOO_LARGE","BBOX_ONLY_FOR_BLOCKS","INVALID_MASKS","INSUFFICIENT_BALANCE","API_KEY_LIMIT_EXCEEDED","WORKER_FAILED"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"Zero-based index of the bad image (INVALID_IMAGE, BLOCK_TOO_LARGE, BBOX_ONLY_FOR_BLOCKS) or mask (INVALID_MASKS)."}}},"ExtChapterCreatedDTO":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid","description":"Request id — the same as the X-Request-Id header; the OCR cost of this chapter shows up under it."},"chapter":{"$ref":"#/components/schemas/ExtChapterDTO"},"images":{"type":"integer","format":"int32","description":"How many source images were uploaded."},"pipeline":{"oneOf":[{"$ref":"#/components/schemas/ExtPipelineDTO"},{"type":"null"}]},"pipelineError":{"oneOf":[{"$ref":"#/components/schemas/ExtChapterErrorDTO"},{"type":"null"}]}}},"ExtChapterDTO":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"number":{"type":["number","null"]},"type":{"type":"string","enum":["MANGA","WEBTOON"]},"readingDirection":{"type":"string","enum":["LTR","RTL"]},"status":{"type":"string","description":"CREATED — only source images, no pages yet; ASSEMBLED — pages are built.","enum":["CREATED","ASSEMBLED"]},"archived":{"type":"boolean"},"pages":{"type":"integer","format":"int32","description":"Pages in the chapter (0 until assembled)."},"ocrDone":{"type":"integer","format":"int32","description":"Pages that already have text blocks (from OCR or added on the website)."},"createdAt":{"type":"string","format":"date-time"}}},"ExtChapterErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["INVALID_PAGINATION","PROJECT_NOT_FOUND","CHAPTER_NOT_FOUND","INVALID_NUMBER","INVALID_READING_DIRECTION","NO_IMAGES","TOO_MANY_IMAGES","TOTAL_TOO_LARGE","INVALID_IMAGE","IMAGE_TOO_TALL","IMAGE_TOO_WIDE","NUMBER_TAKEN","INVALID_UPLOAD_ID","DUPLICATE_UPLOAD","NOT_UPLOADED","UPLOADS_USED","NO_STEPS","ASSEMBLE_REQUIRED","INVALID_WIDTH","PAGES_REQUIRED","INVALID_PAGES","INVALID_LANGUAGE","LANGUAGE_REQUIRED","ASSEMBLE_REJECTED","ALREADY_ASSEMBLED","PIPELINE_RUNNING","INSUFFICIENT_BALANCE","API_KEY_LIMIT_EXCEEDED","NO_PIPELINE"]},"message":{"type":"string"},"index":{"type":["integer","null"],"format":"int32","description":"Zero-based image index for image errors."},"detail":{"type":["string","null"],"description":"ASSEMBLE_REJECTED: TARGET_WIDTH_OUT_OF_RANGE, PAGE_TOO_TALL (a page would be taller than allowed at this width) or ASSET_ASPECT_OUT_OF_RANGE."}}},"ExtPipelineDTO":{"type":"object","description":"Processing of a chapter.","properties":{"id":{"type":"string","format":"uuid"},"chapterId":{"type":"string","format":"uuid"},"requestId":{"type":["string","null"],"format":"uuid","description":"The request that started the processing; its cost — `GET /ext/v1/requests/{requestId}`. null — started on the website."},"status":{"type":"string","description":"DONE may still have items with ERROR, BUSY or PAYMENT_REQUIRED — check `items`.","enum":["RUNNING","DONE","FAILED"]},"wave":{"type":"integer","format":"int32","description":"Current stage: -1 — queued (starts within seconds), 0 — assemble, 1 — OCR; higher — steps started on the website."},"failure":{"type":["string","null"],"description":"FAILED only: PAYMENT_REQUIRED — balance or key limit ran out on the way; ASSEMBLE_FAILED — pages could not be built.","enum":["PAYMENT_REQUIRED","ASSEMBLE_FAILED"]},"items":{"type":"array","description":"One line per step × page. FINISHED means the operation ended; whether text was found is in the chapter's `ocrDone`.","items":{"$ref":"#/components/schemas/ExtPipelineItemDTO"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"}}},"ExtPipelineItemDTO":{"type":"object","properties":{"step":{"type":"string","description":"ASSEMBLE or OCR; other steps come from processing started on the website.","enum":["ASSEMBLE","OCR","CLEAN","TRANSLATE","TYPESET_ZONES","FIND_STYLES","AUTO_FORMS","RENDER","SHARE_LINK"]},"pageId":{"type":["string","null"],"format":"uuid","description":"Page the item is about; null — the whole chapter."},"status":{"type":"string","description":"DISPATCHED — in progress; FINISHED — done; APPLIED — done instantly; SKIPPED — nothing to do; BUSY — the page was busy with another operation; PAYMENT_REQUIRED — not paid; ERROR — failed on our side.","enum":["DISPATCHED","FINISHED","APPLIED","SKIPPED","BUSY","PAYMENT_REQUIRED","ERROR"]}}},"ExtPipelineRequest":{"type":"object","description":"Processing steps; at least one must be true.","properties":{"assemble":{"type":"boolean","description":"Build pages from the images: a webtoon strip is sliced into about `pages` pages, a manga image is one page."},"width":{"type":["integer","null"],"format":"int32","description":"Page width: webtoon 600–800 (default 690), manga 600–1500 (default — the typical image width).","maximum":1500,"minimum":600},"pages":{"type":["integer","null"],"format":"int32","description":"Webtoon only, required with `assemble`: about how many pages to slice into.","maximum":500,"minimum":1},"ocr":{"type":"boolean","description":"Find and read text on every page not read yet. Needs `assemble` until the chapter is assembled."},"language":{"$ref":"#/components/schemas/Language","description":"Source language for `ocr`, not UND. Default — the project's (required if the project has none); a passed value is saved to the project."}}},"ExtChapterFromUploadsRequest":{"type":"object","description":"A chapter from images uploaded with `POST /ext/v1/uploads`, and optional processing steps.","properties":{"projectId":{"type":"string","format":"uuid"},"number":{"type":"number","description":"Chapter number, ≥ 0, may be fractional (e.g. 12.5); unique among the project's active chapters."},"readingDirection":{"type":["string","null"],"description":"Default — the project's.","enum":["LTR","RTL"]},"uploadIds":{"type":"array","description":"`uploadId`s of the uploaded images, in reading order, up to 500.","items":{"type":"string","format":"uuid"},"maxItems":500,"minItems":1},"assemble":{"type":"boolean","description":"Build pages from the images: a webtoon strip is sliced into about `pages` pages, a manga image is one page."},"width":{"type":["integer","null"],"format":"int32","description":"Page width: webtoon 600–800 (default 690), manga 600–1500 (default — the typical image width).","maximum":1500,"minimum":600},"pages":{"type":["integer","null"],"format":"int32","description":"Webtoon only, required with `assemble`: about how many pages to slice into.","maximum":500,"minimum":1},"ocr":{"type":"boolean","description":"Find and read text on every page not read yet. Needs `assemble` until the chapter is assembled."},"language":{"$ref":"#/components/schemas/Language","description":"Source language for `ocr`, not UND. Default — the project's (required if the project has none); a passed value is saved to the project."}}},"ApiRequestDTO":{"type":"object","description":"A logged request. `input` — a summary of what was sent (number of images, bytes, fields); the data itself is not stored.","properties":{"id":{"type":"string","format":"uuid"},"teamId":{"type":"string","format":"uuid"},"apiKeyId":{"type":"string","format":"uuid"},"apiKeyName":{"type":["string","null"],"description":"Key name at read time."},"operation":{"type":"string","enum":["SLICING","OCR","CLEAN","STYLES","CHAPTER_UPLOAD","PIPELINE_START"]},"httpStatus":{"type":["integer","null"],"format":"int32","description":"null — the request is still running or was cut off midway."},"errorCode":{"type":["string","null"]},"input":{"oneOf":[{"$ref":"#/components/schemas/JsonNode"},{"type":"null"}]},"durationMs":{"type":["integer","null"],"format":"int32"},"usage":{"$ref":"#/components/schemas/ApiUsageDTO"},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"}}},"JsonNode":{"type":"object","description":"Any JSON object."},"ExtProjectDTO":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"originalName":{"type":["string","null"]},"type":{"type":"string","enum":["MANGA","WEBTOON"]},"originalLanguage":{"$ref":"#/components/schemas/Language","description":"Source language. UND — not set yet: OCR then needs an explicit `language`."},"targetLanguage":{"$ref":"#/components/schemas/Language"},"readingDirection":{"type":"string","description":"Default reading direction for new chapters (manga).","enum":["LTR","RTL"]},"archived":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"ExtProjectPageDTO":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExtProjectDTO"}},"page":{"type":"integer","format":"int32"},"size":{"type":"integer","format":"int32"},"total":{"type":"integer","format":"int64"}}},"ExtProjectErrorDTO":{"type":"object","properties":{"code":{"type":"string","enum":["INVALID_PAGINATION","PROJECT_NOT_FOUND"]},"message":{"type":"string"}}},"ExtPriceDTO":{"type":"object","properties":{"code":{"type":"string","description":"Price item, as in `usage`: api_slicing_cut, api_slicing_find, api_ocr_mask, api_ocr_text, api_clean, api_clean_premium, api_styles; chapter_processing (webtoon) / chapter_processing_page (manga) — OCR of a chapter."},"name":{"type":"string"},"pricingModel":{"type":"string","description":"PER_UNIT — a usage line costs amount × quantity, but at least one amount. COST_PLUS (api_ocr_text, api_clean_premium) — per unit: our model cost + minAmount, at most amount; only this range is known in advance.","enum":["PER_UNIT","COST_PLUS"]},"unit":{"type":"string","description":"One unit: BLOCK — a text block or cleaned area; NORM_HEIGHT_10K_PX — 10,000 px of height with the image scaled to 690 px width, fractional; PAGE — a page.","enum":["BLOCK","NORM_HEIGHT_10K_PX","PAGE"]},"amount":{"type":"number","description":"Tokens, 6 decimal places: the rate per unit (PER_UNIT) or the upper bound (COST_PLUS)."},"minAmount":{"type":["number","null"],"description":"COST_PLUS only: the markup over our cost, i.e. the lower bound per unit. null otherwise."}}},"ExtPricesDTO":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExtPriceDTO"}},"tokenPrices":{"type":"array","description":"Token price per currency: tokens × pricePerToken = amount in that currency.","items":{"$ref":"#/components/schemas/ExtTokenPriceDTO"}}}},"ExtTokenPriceDTO":{"type":"object","properties":{"currency":{"type":"string","description":"Currency code, e.g. RUB, USD."},"pricePerToken":{"type":"number","description":"Price of 1 token in this currency, 6 decimal places — the rate top-ups are credited at."}}},"ExtKeyDTO":{"type":"object","properties":{"name":{"type":"string"},"keyPrefix":{"type":"string"},"limitValue":{"type":"number","description":"Spending limit in tokens per limitPeriod."},"limitPeriod":{"type":"string","description":"TOTAL — for the key's whole lifetime; DAY/WEEK — renews every day/week.","enum":["TOTAL","DAY","WEEK"]},"usedValue":{"type":"number","description":"Spent in the current window."},"remainingValue":{"type":"number","description":"Still available in the current window."},"resetsOn":{"type":["string","null"],"format":"date","description":"When the limit renews. null for TOTAL."},"expiresAt":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","description":"EXHAUSTED — paid operations are rejected, free ones still work.","enum":["ACTIVE","EXHAUSTED"]}}},"ExtChapterPageDTO":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExtChapterDTO"}},"page":{"type":"integer","format":"int32"},"size":{"type":"integer","format":"int32"},"total":{"type":"integer","format":"int64"}}},"Language":{"type":"string","description":"Language code. UND — not set; operations reject it as a source language.","enum":["AR","AZ","BE","BG","BN","BS","CS","DA","DE","EL","EN","ES","ET","FA","FI","FR","HE","HI","HR","HU","HY","ID","IT","JA","KA","KK","KO","KY","LT","LV","MK","MN","MR","MS","NE","NL","NO","PL","PT","RO","RU","SK","SL","SQ","SR","SV","SW","TA","TE","TG","TH","TL","TR","UK","UND","UR","UZ","VI","ZH"]}},"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"nlk_… (team API key)"}}}}