{
  "openapi": "3.0.3",
  "info": {
    "title": "docsmith",
    "version": "0.21.1",
    "description": "PDF text extraction, layout-recovered Markdown, heading-aware chunks for retrieval, PDF page rendering, and image conversion. Deterministic, no OCR, no model calls, no stored files. Send the document as multipart/form-data field 'file' (20MB limit). Every response carries X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset.",
    "contact": {
      "name": "docsmith support"
    }
  },
  "tags": [
    {
      "name": "Markdown and chunks",
      "description": "Turn a PDF into Markdown with a heading outline, or into chunks that carry that outline path, a page range and a stable id."
    },
    {
      "name": "Text and metadata",
      "description": "Raw text, page ranges and document metadata, without a layout pass."
    },
    {
      "name": "Images",
      "description": "Render PDF pages to PNG or JPEG, and convert or resize any image."
    },
    {
      "name": "Service",
      "description": "Liveness and the engines present on the host."
    }
  ],
  "servers": [
    {
      "url": "https://103-82-194-130.nip.io",
      "description": "production (live; api.mockata.net is an alias once its A record exists)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "bad_request",
                  "unauthorized",
                  "no_file",
                  "empty_file",
                  "not_a_pdf",
                  "unsupported_image",
                  "invalid_parameter",
                  "invalid_pages",
                  "invalid_format",
                  "invalid_dpi",
                  "encrypted_pdf",
                  "page_out_of_range",
                  "page_limit",
                  "pixel_limit",
                  "payload_too_large",
                  "quota_exceeded",
                  "busy",
                  "conversion_failed",
                  "timeout",
                  "not_found",
                  "method_not_allowed",
                  "too_many_chunks",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string",
                "description": "quote this in support requests"
              }
            }
          }
        }
      },
      "PdfText": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "pages": {
            "type": "integer",
            "description": "page count of the whole document"
          },
          "pages_returned": {
            "type": "integer",
            "description": "pages in this response"
          },
          "characters": {
            "type": "integer"
          },
          "lines": {
            "type": "integer"
          },
          "text": {
            "type": "string",
            "description": "pages separated by form feeds"
          }
        }
      },
      "PdfInfo": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "pages": {
            "type": "integer"
          },
          "encrypted": {
            "type": "boolean"
          },
          "page_size": {
            "type": "string",
            "nullable": true,
            "example": "612 x 792 pts"
          },
          "title": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "PdfMarkdown": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "pages": {
            "type": "integer",
            "description": "pages in the whole document"
          },
          "pages_returned": {
            "type": "integer"
          },
          "characters": {
            "type": "integer",
            "description": "length of the Markdown"
          },
          "headings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "level": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 3
                },
                "text": {
                  "type": "string"
                },
                "page": {
                  "type": "integer"
                }
              },
              "required": [
                "level",
                "text",
                "page"
              ]
            }
          },
          "markdown": {
            "type": "string"
          }
        },
        "required": [
          "ok",
          "pages",
          "pages_returned",
          "characters",
          "headings",
          "markdown"
        ]
      },
      "PdfChunks": {
        "type": "object",
        "required": [
          "ok",
          "pages",
          "pages_returned",
          "characters",
          "chunk_count",
          "chunks"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "pages": {
            "type": "integer",
            "description": "pages in the whole document"
          },
          "pages_returned": {
            "type": "integer"
          },
          "characters": {
            "type": "integer",
            "description": "characters across all chunks, overlap included"
          },
          "chunk_count": {
            "type": "integer"
          },
          "chunks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Chunk"
            }
          }
        }
      },
      "Chunk": {
        "type": "object",
        "required": [
          "id",
          "index",
          "heading_path",
          "page_start",
          "page_end",
          "characters",
          "text"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "stable 16 hex chars derived from the content and the heading path; the same PDF yields the same ids on every call"
          },
          "index": {
            "type": "integer",
            "description": "position in this response, from 0"
          },
          "heading_path": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "the outline the chunk sits under, outermost first; empty before the first heading"
          },
          "page_start": {
            "type": "integer"
          },
          "page_end": {
            "type": "integer"
          },
          "characters": {
            "type": "integer"
          },
          "overlap_characters": {
            "type": "integer",
            "description": "present only when overlap_chars was used"
          },
          "text": {
            "type": "string",
            "description": "block content only; the heading path travels as metadata so you decide whether to prepend it"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "missing or invalid X-API-Key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "monthly quota exhausted; X-Quota-Remaining is 0 and Retry-After says when it resets",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "the file or a parameter did not validate; not charged against quota",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "over a hard limit: 20MB upload, 50 pages per call, 50 megapixels per page",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PageOutOfRange": {
        "description": "requested a page past the end of the document; not charged against quota",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "parameters": {}
  },
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "health",
        "summary": "Liveness and available engines",
        "security": [],
        "responses": {
          "200": {
            "description": "healthy"
          }
        },
        "tags": [
          "Service"
        ]
      }
    },
    "/v1/pdf/text": {
      "post": {
        "operationId": "pdfText",
        "summary": "Extract text from a PDF",
        "description": "Returns the text of the selected pages. Scanned PDFs return empty text rather than guessed text; this endpoint does not OCR.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the PDF, max 20MB"
                  },
                  "pages": {
                    "type": "string",
                    "description": "1, 1-3, or 1,4-6. Default: all pages."
                  },
                  "layout": {
                    "type": "boolean",
                    "default": true,
                    "description": "Preserve visual layout."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "extracted text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfText"
                },
                "example": {
                  "characters": 650,
                  "lines": 14,
                  "ok": true,
                  "pages": 2,
                  "pages_returned": 2,
                  "text": "Northwind Q3 Ingestion Report\nPrepared for the retrieval pipeline review, 14 October.\nOverview\nThe corpus grew by 4,120 documents this quarter, almost all of them\nvendor invoices and scanned contracts. Extraction accuracy held at 99.2%\non documents with a text layer.\nFindings\nHeading recovery is the part that moved. Two-column invoices were read\nacross both columns before this quarter and are now read down each one.\n\fCost\nEvery conversion is CPU only, so the marginal cost of the 10,000th page\nis the same as the first. There is no OCR queue and no model call.\nNext steps\nRe-ingest the 2019 archive, then re-measure recall on the same eval set.\n\f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/BadRequest"
          },
          "416": {
            "$ref": "#/components/responses/PageOutOfRange"
          },
          "422": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExceeded"
          }
        },
        "tags": [
          "Text and metadata"
        ]
      }
    },
    "/v1/pdf/markdown": {
      "post": {
        "operationId": "pdfMarkdown",
        "summary": "Convert a PDF to Markdown for LLM ingestion",
        "description": "Returns the selected pages as Markdown: heading levels recovered from relative font size and weight, paragraphs rejoined across visual line breaks, list markers normalised, running page numbers removed, and a `---` boundary between pages. Anything ambiguous stays a plain paragraph rather than a guessed heading. Scanned pages have no text layer and return a 422 `no_text_layer` error instead of an empty document; this endpoint does not OCR.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the PDF, max 20MB"
                  },
                  "pages": {
                    "type": "string",
                    "description": "1, 1-3, or 1,4-6. Default: all pages."
                  },
                  "page_markers": {
                    "type": "boolean",
                    "default": true,
                    "description": "Emit a horizontal rule between pages."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the document as Markdown, plus the headings it implies",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfMarkdown"
                },
                "example": {
                  "characters": 676,
                  "headings": [
                    {
                      "level": 1,
                      "page": 1,
                      "text": "Northwind Q3 Ingestion Report"
                    },
                    {
                      "level": 2,
                      "page": 1,
                      "text": "Overview"
                    },
                    {
                      "level": 2,
                      "page": 1,
                      "text": "Findings"
                    },
                    {
                      "level": 2,
                      "page": 2,
                      "text": "Cost"
                    },
                    {
                      "level": 2,
                      "page": 2,
                      "text": "Next steps"
                    }
                  ],
                  "markdown": "# Northwind Q3 Ingestion Report\n\nPrepared for the retrieval pipeline review, 14 October.\n\n## Overview\n\nThe corpus grew by 4,120 documents this quarter, almost all of them vendor invoices and scanned contracts. Extraction accuracy held at 99.2% on documents with a text layer.\n\n## Findings\n\nHeading recovery is the part that moved. Two-column invoices were read across both columns before this quarter and are now read down each one.\n\n---\n\n## Cost\n\nEvery conversion is CPU only, so the marginal cost of the 10,000th page is the same as the first. There is no OCR queue and no model call.\n\n## Next steps\n\nRe-ingest the 2019 archive, then re-measure recall on the same eval set.\n",
                  "ok": true,
                  "pages": 2,
                  "pages_returned": 2
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/BadRequest"
          },
          "416": {
            "$ref": "#/components/responses/PageOutOfRange"
          },
          "422": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExceeded"
          }
        },
        "tags": [
          "Markdown and chunks"
        ]
      }
    },
    "/v1/pdf/info": {
      "post": {
        "operationId": "pdfInfo",
        "summary": "Page count, page size, encryption and title",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the PDF, max 20MB"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "document metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfInfo"
                },
                "example": {
                  "encrypted": false,
                  "ok": true,
                  "page_size": "612 x 792 pts (letter)",
                  "pages": 2,
                  "title": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExceeded"
          }
        },
        "tags": [
          "Text and metadata"
        ]
      }
    },
    "/v1/pdf/images": {
      "post": {
        "operationId": "pdfImages",
        "summary": "Render PDF pages to PNG or JPEG",
        "description": "One page returns the image itself; several pages return a zip archive named pages.zip.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the PDF, max 20MB"
                  },
                  "pages": {
                    "type": "string",
                    "description": "1, 1-3, or 1,4-6. Default: all pages."
                  },
                  "dpi": {
                    "type": "integer",
                    "minimum": 72,
                    "maximum": 300,
                    "default": 150
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "png",
                      "jpeg"
                    ],
                    "default": "png"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One page returns an image; several return a zip archive.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/BadRequest"
          },
          "416": {
            "$ref": "#/components/responses/PageOutOfRange"
          },
          "422": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExceeded"
          }
        },
        "tags": [
          "Images"
        ]
      }
    },
    "/v1/image/convert": {
      "post": {
        "operationId": "imageConvert",
        "summary": "Convert, resize, and strip metadata from an image",
        "description": "Accepts JPEG, PNG, GIF, BMP or TIFF. Resizing preserves aspect ratio; max_width and max_height are a bounding box, never an upscale.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the image, max 20MB"
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "jpeg",
                      "png",
                      "webp",
                      "gif"
                    ],
                    "default": "jpeg"
                  },
                  "quality": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 82,
                    "description": "JPEG/WebP quality"
                  },
                  "max_width": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 12000
                  },
                  "max_height": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 12000
                  },
                  "strip": {
                    "type": "boolean",
                    "default": true,
                    "description": "Drop EXIF and other metadata."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "converted image; X-Image-Width and X-Image-Height describe the result",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/gif": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/QuotaExceeded"
          }
        },
        "tags": [
          "Images"
        ]
      }
    },
    "/v1/pdf/chunks": {
      "post": {
        "operationId": "pdfChunks",
        "summary": "Split a PDF into heading-aware chunks for a retrieval pipeline",
        "description": "Returns chunks that end where the document ends a thought, not where a character counter runs out. A heading always opens a new chunk and the chunk carries the outline path it belongs to, so a retriever can cite the section and a re-ingest does not duplicate rows. Paragraph and heading boundaries come from the same layout recovery as /v1/pdf/markdown. Oversize paragraphs split at sentence boundaries. Scanned pages return 422 `no_text_layer`; this endpoint does not OCR.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the PDF, max 20MB"
                  },
                  "pages": {
                    "type": "string",
                    "description": "1, 1-3, or 1,4-6. Default: all pages."
                  },
                  "target_chars": {
                    "type": "integer",
                    "default": 1800,
                    "minimum": 200,
                    "maximum": 20000,
                    "description": "soft size target for a chunk; a heading, not the counter, always wins"
                  },
                  "overlap_chars": {
                    "type": "integer",
                    "default": 0,
                    "minimum": 0,
                    "maximum": 1000,
                    "description": "repeat this many trailing characters of the previous chunk at the start of the next"
                  },
                  "min_chars": {
                    "type": "integer",
                    "default": 0,
                    "minimum": 0,
                    "maximum": 20000,
                    "description": "merge a chunk shorter than this into the one before it"
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the document as chunks, with the outline path of each",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfChunks"
                },
                "example": {
                  "characters": 579,
                  "chunk_count": 5,
                  "chunks": [
                    {
                      "characters": 55,
                      "heading_path": [
                        "Northwind Q3 Ingestion Report"
                      ],
                      "id": "092d679b5bf450ba",
                      "index": 0,
                      "page_end": 1,
                      "page_start": 1,
                      "text": "Prepared for the retrieval pipeline review, 14 October."
                    },
                    {
                      "characters": 172,
                      "heading_path": [
                        "Northwind Q3 Ingestion Report",
                        "Overview"
                      ],
                      "id": "7556b5e6e48fb8e8",
                      "index": 1,
                      "page_end": 1,
                      "page_start": 1,
                      "text": "The corpus grew by 4,120 documents this quarter, almost all of them vendor invoices and scanned contracts. Extraction accuracy held at 99.2% on documents with a text layer."
                    },
                    {
                      "characters": 142,
                      "heading_path": [
                        "Northwind Q3 Ingestion Report",
                        "Findings"
                      ],
                      "id": "0a50143d64f65016",
                      "index": 2,
                      "page_end": 1,
                      "page_start": 1,
                      "text": "Heading recovery is the part that moved. Two-column invoices were read across both columns before this quarter and are now read down each one."
                    },
                    {
                      "characters": 138,
                      "heading_path": [
                        "Northwind Q3 Ingestion Report",
                        "Cost"
                      ],
                      "id": "dad9cc1935ac2ecf",
                      "index": 3,
                      "page_end": 2,
                      "page_start": 2,
                      "text": "Every conversion is CPU only, so the marginal cost of the 10,000th page is the same as the first. There is no OCR queue and no model call."
                    },
                    {
                      "characters": 72,
                      "heading_path": [
                        "Northwind Q3 Ingestion Report",
                        "Next steps"
                      ],
                      "id": "f60d4b42b1a56852",
                      "index": 4,
                      "page_end": 2,
                      "page_start": 2,
                      "text": "Re-ingest the 2019 archive, then re-measure recall on the same eval set."
                    }
                  ],
                  "ok": true,
                  "pages": 2,
                  "pages_returned": 2
                }
              }
            }
          },
          "413": {
            "description": "more than 500 chunks in one call; raise target_chars or narrow pages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Markdown and chunks"
        ]
      }
    }
  }
}
