{
  "openapi": "3.1.0",
  "info": {
    "title": "Recto OCR API",
    "version": "1.0.0",
    "summary": "OCR that returns a box and a confidence for every word.",
    "description": "Send a PDF or image, get back its text with word-level geometry, as JSON, a searchable PDF or hOCR. One synchronous call: no job to poll and no webhook to configure.\n\nPages are metered per plan. An image is one page and a PDF is one page per page. The precise engine draws 20 pages of allowance per page read. Failed recognitions are never billed."
  },
  "servers": [
    { "url": "https://api.recto.dev", "description": "Hosted" },
    { "url": "http://localhost:8080", "description": "Self-hosted default" }
  ],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "OCR" },
    { "name": "Service" }
  ],
  "paths": {
    "/v1/ocr": {
      "post": {
        "tags": ["OCR"],
        "operationId": "recognize",
        "summary": "Recognise a document",
        "description": "Send the file as multipart field `file`, or as the raw request body. Options may be query parameters or multipart fields.",
        "parameters": [
          { "$ref": "#/components/parameters/Engine" },
          { "$ref": "#/components/parameters/Lang" },
          { "$ref": "#/components/parameters/Fields" },
          { "$ref": "#/components/parameters/Output" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "format": "binary", "description": "PDF, PNG, JPEG, HEIC/HEIF, GIF, WebP, TIFF or BMP." },
                  "engine": { "$ref": "#/components/schemas/EngineName" },
                  "lang": { "type": "string" },
                  "fields": { "type": "string", "enum": ["0", "1"] },
                  "output": { "$ref": "#/components/schemas/OutputName" }
                }
              }
            },
            "application/pdf": { "schema": { "type": "string", "format": "binary" } },
            "image/*": { "schema": { "type": "string", "format": "binary" } },
            "application/octet-stream": { "schema": { "type": "string", "format": "binary" } }
          }
        },
        "responses": {
          "200": {
            "description": "The document, read. With `output=json` (the default) the body is JSON. With `output=pdf` it is a searchable PDF, and with `output=hocr` an hOCR document; metering then travels in headers only.",
            "headers": {
              "X-Recto-Pages-Used": { "$ref": "#/components/headers/PagesUsed" },
              "X-Recto-Pages-Quota": { "$ref": "#/components/headers/PagesQuota" },
              "X-Recto-Job-Id": { "description": "File outputs only.", "schema": { "type": "integer" } },
              "X-Recto-Pages-Billed": { "description": "File outputs only. Allowance this request consumed.", "schema": { "type": "integer" } },
              "X-Recto-Page-Count": { "description": "File outputs only. Pages actually read.", "schema": { "type": "integer" } },
              "X-Recto-Mean-Conf": { "description": "File outputs only. Word-weighted mean confidence, 0 to 100.", "schema": { "type": "number" } }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/OCRResponse" } },
              "application/pdf": { "schema": { "type": "string", "format": "binary" } },
              "text/html": { "schema": { "type": "string", "description": "hOCR 1.2" } }
            }
          },
          "400": { "$ref": "#/components/responses/Error", "description": "`bad_image`, `unknown_engine`, `unknown_output` or `unsupported_output`." },
          "401": { "$ref": "#/components/responses/Error", "description": "`missing_key` or `invalid_key`." },
          "402": { "$ref": "#/components/responses/Error", "description": "`plan_required`, `quota_exceeded`, `precise_quota_exceeded` or `document_too_long`. Refused before any work, and not billed." },
          "422": { "$ref": "#/components/responses/Error", "description": "`ocr_failed`. The request was readable but recognition failed. Not billed." },
          "429": {
            "description": "`rate_limited`. Honour Retry-After.",
            "headers": { "Retry-After": { "schema": { "type": "integer" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": { "$ref": "#/components/responses/Error", "description": "`engine_unavailable`. The precise engine is down; retry or use engine=fast." }
        }
      }
    },
    "/v1/health": {
      "get": {
        "tags": ["Service"],
        "operationId": "health",
        "summary": "Service status and which engines are available",
        "security": [],
        "responses": {
          "200": {
            "description": "Up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "engines"],
                  "properties": {
                    "status": { "type": "string", "const": "ok" },
                    "engines": {
                      "type": "object",
                      "properties": {
                        "fast": { "type": "boolean" },
                        "precise": { "type": "boolean" },
                        "auto": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/plans": {
      "get": {
        "tags": ["Service"],
        "operationId": "listPlans",
        "summary": "The plan ladder and its limits",
        "security": [],
        "responses": {
          "200": {
            "description": "Every plan for sale, cheapest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "plans": { "type": "array", "items": { "$ref": "#/components/schemas/Plan" } } }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the dashboard, sent as `Authorization: Bearer rk_live_…`."
      }
    },
    "parameters": {
      "Engine": {
        "name": "engine", "in": "query", "required": false,
        "description": "`auto` (default where available) reads a PDF's own text layer and OCRs only scanned pages. `fast` runs Tesseract on every page and always returns per-word boxes. `precise` runs a vision model: handwriting and dense layouts, Markdown output, no boxes, Pro plan and above.",
        "schema": { "$ref": "#/components/schemas/EngineName" }
      },
      "Lang": {
        "name": "lang", "in": "query", "required": false,
        "description": "Tesseract language code, fast engine only. Several as `eng+deu`.",
        "schema": { "type": "string", "default": "eng" }
      },
      "Fields": {
        "name": "fields", "in": "query", "required": false,
        "description": "Set to 1 to infer form fields (label/value pairs and ticked boxes) from the page layout.",
        "schema": { "type": "string", "enum": ["0", "1"], "default": "0" }
      },
      "Output": {
        "name": "output", "in": "query", "required": false,
        "description": "`json` (default), `pdf` for a searchable PDF (the page image with an invisible text layer), or `hocr`. File outputs need word positions, so the precise engine refuses them with `unsupported_output`. Pages that `auto` read from a PDF's text layer are copied into the searchable PDF unchanged.",
        "schema": { "$ref": "#/components/schemas/OutputName" }
      }
    },
    "headers": {
      "PagesUsed": { "description": "Allowance used this month, including this request.", "schema": { "type": "integer" } },
      "PagesQuota": { "description": "The plan's monthly allowance.", "schema": { "type": "integer" } }
    },
    "responses": {
      "Error": {
        "description": "An error, in the standard envelope.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "EngineName": { "type": "string", "enum": ["auto", "fast", "precise"] },
      "OutputName": { "type": "string", "enum": ["json", "pdf", "hocr"], "default": "json" },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable; branch on this, not on the message.",
                "enum": ["bad_image", "unknown_engine", "unknown_output", "unsupported_output", "missing_key", "invalid_key", "plan_required", "quota_exceeded", "precise_quota_exceeded", "document_too_long", "ocr_failed", "rate_limited", "engine_unavailable", "internal"]
              },
              "message": { "type": "string" }
            }
          }
        }
      },
      "OCRResponse": {
        "type": "object",
        "required": ["id", "result", "pages_billed", "page_multiplier", "pages_used", "quota"],
        "properties": {
          "id": { "type": "integer", "description": "Job id, as shown in the dashboard's activity log." },
          "result": { "$ref": "#/components/schemas/Result" },
          "pages_billed": { "type": "integer", "description": "Allowance consumed; already multiplied for the precise engine." },
          "page_multiplier": { "type": "integer", "description": "1 for auto and fast, 20 for precise." },
          "pages_used": { "type": "integer" },
          "quota": { "type": "integer" }
        }
      },
      "Result": {
        "type": "object",
        "required": ["engine", "source", "page_count", "text", "word_count", "mean_conf", "duration_ms", "pages"],
        "properties": {
          "engine": { "$ref": "#/components/schemas/EngineName" },
          "source": { "type": "string", "enum": ["image", "pdf"] },
          "page_count": { "type": "integer" },
          "text": { "type": "string", "description": "The whole document in reading order, pages separated by a blank line." },
          "markdown": { "type": "string", "description": "Layout-aware Markdown. Precise engine only." },
          "word_count": { "type": "integer" },
          "mean_conf": { "type": "number", "description": "Word-weighted mean confidence, 0 to 100." },
          "duration_ms": { "type": "integer" },
          "field_count": { "type": "integer" },
          "pages": { "type": "array", "items": { "$ref": "#/components/schemas/Page" }, "description": "Always an array, one entry per page, even for a single image." },
          "routing": { "$ref": "#/components/schemas/Routing" }
        }
      },
      "Page": {
        "type": "object",
        "required": ["page", "word_level", "width", "height", "text", "words", "word_count", "mean_conf"],
        "properties": {
          "page": { "type": "integer", "description": "1-indexed." },
          "source": { "type": "string", "enum": ["ocr", "text"], "description": "`text` when read from the PDF's own text layer." },
          "word_level": { "type": "boolean", "description": "True when `words` holds one box per word. False on text-layer pages, where boxes are per text run." },
          "width": { "type": "integer", "description": "Page width in pixels; box coordinates are in this space." },
          "height": { "type": "integer" },
          "text": { "type": "string" },
          "markdown": { "type": "string" },
          "words": { "type": "array", "items": { "$ref": "#/components/schemas/Word" }, "description": "In reading order. Empty on the precise engine." },
          "word_count": { "type": "integer" },
          "mean_conf": { "type": "number" },
          "fields": { "type": "array", "items": { "$ref": "#/components/schemas/Field" } }
        }
      },
      "Word": {
        "type": "object",
        "required": ["text", "x", "y", "w", "h", "conf", "block", "line"],
        "properties": {
          "text": { "type": "string" },
          "x": { "type": "integer", "description": "Left edge, pixels from the page's left." },
          "y": { "type": "integer", "description": "Top edge, pixels from the page's top." },
          "w": { "type": "integer" },
          "h": { "type": "integer" },
          "conf": { "type": "number", "description": "0 to 100." },
          "block": { "type": "integer" },
          "line": { "type": "integer", "description": "Words sharing block and line are on the same rendered line." }
        }
      },
      "Box": {
        "type": "object",
        "required": ["x", "y", "w", "h"],
        "properties": {
          "x": { "type": "integer" }, "y": { "type": "integer" },
          "w": { "type": "integer" }, "h": { "type": "integer" }
        }
      },
      "Field": {
        "type": "object",
        "required": ["label", "kind", "label_box"],
        "properties": {
          "label": { "type": "string" },
          "value": { "type": "string" },
          "kind": { "type": "string", "enum": ["value", "checkbox"] },
          "checked": { "type": "boolean", "description": "Only ever true: an unticked box leaves no mark on a flattened page." },
          "label_box": { "$ref": "#/components/schemas/Box" },
          "value_box": { "$ref": "#/components/schemas/Box" }
        }
      },
      "Routing": {
        "type": "object",
        "description": "Which pages the auto engine read from the text layer and which it OCRed.",
        "properties": {
          "pdf_type": { "type": "string" },
          "pages_extracted": { "type": "integer" },
          "pages_ocred": { "type": "integer" },
          "pages_rasterised": { "type": "integer" },
          "detection_time_ms": { "type": "integer" },
          "raster_time_ms": { "type": "integer" }
        }
      },
      "Plan": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "price_cents": { "type": "integer" },
          "pages_month": { "type": "integer" },
          "max_keys": { "type": "integer" },
          "max_upload_mb": { "type": "integer" },
          "max_doc_pages": { "type": "integer" },
          "engines": { "type": "array", "items": { "type": "string", "enum": ["fast", "precise"] } },
          "rate_per_min": { "type": "integer" },
          "blurb": { "type": "string" },
          "per_thousand_cents": { "type": "integer" },
          "precise_pages_month": { "type": "integer" }
        }
      }
    }
  }
}
