{
  "openapi": "3.1.0",
  "info": {
    "title": "pdfmd.dev: PDF to Markdown API",
    "version": "1.0.0",
    "summary": "Convert PDFs to clean Markdown with headings, tables and page counts.",
    "description": "Send a PDF and get Markdown back as JSON. The converter keeps headings and reading order and writes tables as GitHub-flavoured Markdown. Each answer has the page count, the character offset of every page start and a list of values to check.\n\nUpload a file or give a public link. For scanned English PDFs use the OCR endpoint. Both endpoints count pages: one page credit per page.\n\nExample values in this document are illustrations. The ids and counts in the examples are not from a real file.\n\n## Limits and plans\n\nFree service is also subject to shared daily capacity. If it is exhausted, retry after the time in Retry-After. Paid plan allowances remain separate.\n\nA request with no key works. It is the anonymous tier. A key raises the ceiling and the per-file page limit.\n\n- No key: 25 pages a day per IP address, up to 25 pages in one file. The count resets at 00:00 UTC.\n- Free key: 50 pages a day, up to 25 pages in one file. Get one with your email at https://pdfmd.dev/billing/free. Free use is for one person: your own projects or your studies. Production, team and commercial use need a paid plan.\n- Starter: $8.90 a month for 10,000 pages.\n- Pro: $75 a month for 100,000 pages.\n- Starter works out to $0.89 per 1,000 pages.\n\nEvery answer from a counted route carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds), and `X-RateLimit-Unit` (pages). Over the limit, the route answers 429 with a `pricing` link and, without a key, a `free_key` link.\nPlans and prices: https://pdfmd.dev/pricing.\nTerms: https://pdfmd.dev/terms. Privacy: https://pdfmd.dev/privacy. Contact: hello@darkvectorcognition.ai.",
    "termsOfService": "https://pdfmd.dev/terms",
    "contact": {
      "name": "Dark Vector Cognition",
      "email": "hello@darkvectorcognition.ai",
      "url": "https://pdfmd.dev/docs"
    }
  },
  "servers": [
    {
      "url": "https://pdfmd.dev",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "pdfmd.dev: PDF to Markdown API guide",
    "url": "https://pdfmd.dev/docs"
  },
  "security": [
    {},
    {
      "ApiKeyAuth": []
    },
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Convert",
      "description": "Turn a PDF into Markdown. Counted in pages."
    },
    {
      "name": "Account",
      "description": "Read your own allowance. Uses none of it."
    }
  ],
  "paths": {
    "/api/v1/convert": {
      "post": {
        "operationId": "convertPdf",
        "summary": "Convert a PDF to Markdown",
        "description": "Send a PDF as a multipart upload (field `file`) or give a public link in a JSON body. The answer is JSON with the Markdown, the page count and parser notes. Set `format` to `html` for the document as an HTML fragment instead, or `both` for the two.\n\nAn upload is at most 4 MB. A PDF at a link is at most 50 MB. The JSON answer is at most 4 MB.\n\nOne page credit per page. Without a key a file may have 25 pages, with a free key 25, with a plan 400.\n\nPages are held before the work starts and settled to the real count after. A failed conversion releases the hold and charges nothing.",
        "tags": [
          "Convert"
        ],
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "The same as the `format` field of the body, for a caller that finds a URL parameter easier.",
            "schema": {
              "type": "string",
              "enum": [
                "markdown",
                "html",
                "both"
              ],
              "default": "markdown"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "contentMediaType": "application/pdf",
                    "description": "The PDF, at most 4 MB."
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "markdown",
                      "html",
                      "both"
                    ],
                    "default": "markdown",
                    "description": "Which text of the document to return. `markdown` (the default) returns `markdown`. `html` returns the same document as an HTML fragment in `html` and no `markdown`. `both` returns both fields. The multipart field or JSON field wins over a `?format=` query parameter of the same name. Any other value is a 400 `bad_format`. A caller without a key or with a free key whose HTML renders fail 5 times in a UTC day gets 429 `render_limit` for `html` and `both` until the next day. Paid plans are not limited this way."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConvertUrlRequest"
              },
              "example": {
                "url": "https://arxiv.org/pdf/1706.03762"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The converted document. `X-RateLimit-Remaining` is the pages left after this request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertResult"
                },
                "example": {
                  "id": "9f2c4e7a1b3d5f60",
                  "parserVersion": "0.1.2",
                  "title": "Attention Is All You Need",
                  "pages": 15,
                  "scannedPages": 0,
                  "tables": 4,
                  "chars": 41616,
                  "ms": 5700,
                  "pageBreaks": [
                    0,
                    3120,
                    6410
                  ],
                  "warnings": [],
                  "markdown": "# Attention Is All You Need\n\n## Abstract\n\nThe dominant sequence transduction models are based on complex recurrent or convolutional neural networks..."
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "X-RateLimit-Unit": {
                "$ref": "#/components/headers/XRateLimitUnit"
              },
              "X-Caller": {
                "$ref": "#/components/headers/XCaller"
              }
            }
          },
          "400": {
            "description": "No PDF was sent (`missing_file`), no link was sent (`missing_url`) or `format` is not `markdown`, `html` or `both` (`bad_format`). Nothing is converted and nothing is charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_file": {
                    "value": {
                      "error": "missing_file",
                      "message": "Send the PDF as multipart field 'file'."
                    }
                  },
                  "missing_url": {
                    "value": {
                      "error": "missing_url",
                      "message": "Send JSON {\"url\": \"https://...\"} or a multipart file."
                    }
                  },
                  "bad_format": {
                    "value": {
                      "error": "bad_format",
                      "message": "format must be markdown, html or both"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "402": {
            "$ref": "#/components/responses/TrialEnded"
          },
          "413": {
            "description": "The upload is over 4 MB or the JSON body is over 64 KB (`too_large`), or the file has more pages than this caller may convert in one request (`file_too_long`, with `pages` and `maxPagesPerFile`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TooLarge"
                    },
                    {
                      "$ref": "#/components/schemas/RateLimitError"
                    }
                  ]
                },
                "examples": {
                  "too_large": {
                    "value": {
                      "error": "too_large",
                      "message": "PDF larger than 4 MB (4,000,000 bytes), the most an upload can be here. A PDF at a public link can be up to 50 MB: convert the link instead."
                    }
                  },
                  "file_too_long": {
                    "value": {
                      "error": "file_too_long",
                      "message": "This PDF has 40 pages. Free conversions take up to 25 pages per file.",
                      "free_key": "https://pdfmd.dev/billing/free",
                      "pricing": "https://pdfmd.dev/pricing",
                      "recover": "https://pdfmd.dev/billing/regenerate",
                      "pages": 40,
                      "maxPagesPerFile": 25
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The converted result is over 4 MB, or, with `format` `html` or `both`, the Markdown is over 1,000,000 bytes, the most the API renders as HTML, or it nests quotes or lists more than 100 levels deep (`maxResultBytes` says which limit applied; for nesting it is the size limit). No pages are charged. Ask for `format=markdown`, or try a shorter extract of the PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultTooLarge"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The converter failed or returned something that is not a valid document (`worker_unreachable`, `worker_invalid_response`), or, with `format` `html` or `both`, the document could not be rendered as HTML within its time and memory limits (`render_failed`). The pages held for the request are released. For `render_failed`, ask for `format=markdown`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "worker_unreachable",
                  "message": "The converter failed before returning a document."
                }
              }
            }
          },
          "503": {
            "description": "The gate could not verify the key (`billing_store_unavailable`), usage cannot be checked (`quota_store_unavailable`), today's shared free capacity is exhausted (`free_capacity_reached`), pages cannot be reserved right now (`page_accounting_unavailable`, with `Retry-After`), or the converter is not configured (`parser_unavailable`). Nothing is converted and nothing is charged. With `format` `html` or `both`, the HTML renderer can also answer 503 after the conversion: it is busy (`render_busy`, with `Retry-After`) or not deployed (`render_unavailable`). Nothing is charged for either.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "page_accounting_unavailable",
                  "message": "Pages cannot be reserved right now, so nothing was converted and nothing was charged. Please retry shortly."
                }
              }
            }
          },
          "504": {
            "description": "The converter did not answer within 110 seconds (`worker_unreachable`). The pages held for the request are released. A shorter extract of the PDF may convert.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "worker_unreachable",
                  "message": "The converter did not answer within 110 seconds. Nothing was charged. A shorter extract of the PDF may convert."
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ocr": {
      "post": {
        "operationId": "ocrPdf",
        "summary": "OCR a scanned PDF (English)",
        "description": "For scanned PDFs. Existing text layers are kept and image-only pages are read with OCR. English printed text only. Names, numbers, tables and unusual layouts need checking against the source.\n\nSet `output` to `markdown` (the default) for the usual JSON plus OCR fields, or `searchable_pdf` for PDF bytes. With `markdown`, `format` works as on /api/v1/convert: `html` returns the document as an HTML fragment instead of Markdown, `both` returns the two. `searchable_pdf` ignores `format`. For `searchable_pdf` check the HTTP status before you save the body: an error answer is JSON.\n\nUses the same page allowance as /api/v1/convert, one page credit per page, including pages that already have text. A request can take up to five minutes. After a timeout, call GET /api/v1/me before you retry.\n\nIf an OCR conversion fails after it was sent to the converter, a caller without a key or with a free key is charged the pages held for it, because a failed OCR job still uses the converter's time. Paid plans get the pages back. A request the converter refuses before it starts OCR is not charged: a link that is private, malformed or cannot be fetched, a body or PDF over the size limit, a file that is not a PDF, or a body that arrives too slowly. The error message says which one happened.\n\nIf OCR is not enabled on the deployment, the route answers 503 `ocr_unavailable`.",
        "tags": [
          "Convert"
        ],
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "The same as the `format` field of the body, for a caller that finds a URL parameter easier. Applies when `output` is `markdown`.",
            "schema": {
              "type": "string",
              "enum": [
                "markdown",
                "html",
                "both"
              ],
              "default": "markdown"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "contentMediaType": "application/pdf"
                  },
                  "output": {
                    "type": "string",
                    "enum": [
                      "markdown",
                      "searchable_pdf"
                    ],
                    "default": "markdown"
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "markdown",
                      "html",
                      "both"
                    ],
                    "default": "markdown",
                    "description": "Which text of the document to return. `markdown` (the default) returns `markdown`. `html` returns the same document as an HTML fragment in `html` and no `markdown`. `both` returns both fields. Applies when `output` is `markdown`; `searchable_pdf` ignores it. The multipart field or JSON field wins over a `?format=` query parameter of the same name. Any other value is a 400 `bad_format`. A caller without a key or with a free key whose HTML renders fail 5 times in a UTC day gets 429 `render_limit` for `html` and `both` until the next day. Paid plans are not limited this way."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OcrUrlRequest"
              },
              "example": {
                "url": "https://example.org/scan.pdf",
                "output": "markdown"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`markdown`: the document JSON with the OCR fields. `searchable_pdf`: the PDF bytes.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "X-RateLimit-Unit": {
                "$ref": "#/components/headers/XRateLimitUnit"
              },
              "X-Caller": {
                "$ref": "#/components/headers/XCaller"
              },
              "X-Pdfmd-Pages": {
                "description": "Pages in the document.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Pdfmd-Ocr-Pages": {
                "description": "Image-only pages that OCR read.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Pdfmd-Scanned-Pages": {
                "description": "Image pages with very little readable text after OCR. They may be blank or unreadable; review the original. All pages remain in the PDF.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-Pdfmd-Rx": {
                "description": "Always `ocr`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "ocr"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OcrResult"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "400": {
            "description": "No PDF (`missing_file`), no link (`missing_url`), an unknown `output` (`invalid_output`) or, with `output` `markdown`, an unknown `format` (`bad_format`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_output",
                  "message": "Choose output 'markdown' or 'searchable_pdf'."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "402": {
            "$ref": "#/components/responses/TrialEnded"
          },
          "413": {
            "description": "The upload is over 4 MB or the JSON body is over 64 KB (`too_large`), or the file has more pages than this caller may convert in one request (`file_too_long`, with `pages` and `maxPagesPerFile`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TooLarge"
                    },
                    {
                      "$ref": "#/components/schemas/RateLimitError"
                    }
                  ]
                },
                "examples": {
                  "too_large": {
                    "value": {
                      "error": "too_large",
                      "message": "PDF larger than 4 MB (4,000,000 bytes), the most an upload can be here. A PDF at a public link can be up to 50 MB: convert the link instead."
                    }
                  },
                  "file_too_long": {
                    "value": {
                      "error": "file_too_long",
                      "message": "This PDF has 40 pages. Free conversions take up to 25 pages per file.",
                      "free_key": "https://pdfmd.dev/billing/free",
                      "pricing": "https://pdfmd.dev/pricing",
                      "recover": "https://pdfmd.dev/billing/regenerate",
                      "pages": 40,
                      "maxPagesPerFile": 25
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The converted result is over 4 MB, or, with `format` `html` or `both`, the Markdown is over 1,000,000 bytes, the most the API renders as HTML, or it nests quotes or lists more than 100 levels deep. Paid plans are not charged. A caller without a key or with a free key is charged the pages held, because the OCR work is done. Ask for `format=markdown`, or try a shorter extract of the PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultTooLarge"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The converter failed or returned something that is not a valid document (`worker_unreachable`, `worker_invalid_response`), or, with `format` `html` or `both`, the document could not be rendered as HTML (`render_failed`). Paid plans get the pages held back. A caller without a key or with a free key is charged them, unless the converter was never reached or refused the request before it started OCR. A `render_failed` is charged this way too, because the OCR work is done; ask for `format=markdown`. The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "worker_unreachable",
                  "message": "The converter failed before returning a document."
                }
              }
            }
          },
          "503": {
            "description": "OCR is not enabled (`ocr_unavailable`), the key or usage could not be verified, today's shared free capacity is exhausted (`free_capacity_reached`), or pages cannot be reserved. Nothing is charged.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "ocr_unavailable",
                  "message": "OCR is not enabled on this deployment."
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ocr/session": {
      "post": {
        "operationId": "createOcrUploadSession",
        "summary": "Authorize a large OCR upload",
        "description": "When enabled, reserves the caller's existing page allowance and returns a single-use upload session. Within two minutes, POST multipart field file directly to url with header X-Ocr-Session set to token. Do not send your API key to that URL. Upload and PDF output limits are 52,428,800 bytes; Markdown keeps its 4 MB limit. For long jobs, send Accept: text/event-stream. The response sends keepalive comments, then a meta event with JSON status, headers and byte count, base64 chunk events, and a done event with {}. Reassemble chunks only after done and verify the byte count. Job errors use the status and JSON body inside those frames. Without this Accept header, the response contains PDF bytes or document JSON and OCR/quota headers; long jobs may exceed the hosting response deadline. Page credits settle to the actual count. Unused holds expire within 15 minutes. No public file link is created. Returns 503 when unavailable.",
        "tags": [
          "Convert"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "output"
                ],
                "properties": {
                  "output": {
                    "type": "string",
                    "enum": [
                      "markdown",
                      "searchable_pdf"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Single-use upload authorization. Keep token private.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "token",
                    "maxPages",
                    "maxBytes",
                    "expiresAt"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "token": {
                      "type": "string",
                      "pattern": "^[a-f0-9]{64}$"
                    },
                    "maxPages": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 400
                    },
                    "maxBytes": {
                      "type": "integer",
                      "enum": [
                        52428800
                      ]
                    },
                    "expiresAt": {
                      "type": "integer",
                      "description": "Upload authorization expiry, Unix milliseconds."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "X-RateLimit-Unit": {
                "$ref": "#/components/headers/XRateLimitUnit"
              },
              "X-Caller": {
                "$ref": "#/components/headers/XCaller"
              }
            }
          },
          "400": {
            "description": "Invalid output or request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "402": {
            "$ref": "#/components/responses/TrialEnded"
          },
          "413": {
            "description": "The upload is over 4 MB or the JSON body is over 64 KB (`too_large`), or the file has more pages than this caller may convert in one request (`file_too_long`, with `pages` and `maxPagesPerFile`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TooLarge"
                    },
                    {
                      "$ref": "#/components/schemas/RateLimitError"
                    }
                  ]
                },
                "examples": {
                  "too_large": {
                    "value": {
                      "error": "too_large",
                      "message": "PDF larger than 4 MB (4,000,000 bytes), the most an upload can be here. A PDF at a public link can be up to 50 MB: convert the link instead."
                    }
                  },
                  "file_too_long": {
                    "value": {
                      "error": "file_too_long",
                      "message": "This PDF has 40 pages. Free conversions take up to 25 pages per file.",
                      "free_key": "https://pdfmd.dev/billing/free",
                      "pricing": "https://pdfmd.dev/pricing",
                      "recover": "https://pdfmd.dev/billing/regenerate",
                      "pages": 40,
                      "maxPagesPerFile": 25
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The converted result is over 4 MB, or, with `format` `html` or `both`, the Markdown is over 1,000,000 bytes, the most the API renders as HTML, or it nests quotes or lists more than 100 levels deep (`maxResultBytes` says which limit applied; for nesting it is the size limit). No pages are charged. Ask for `format=markdown`, or try a shorter extract of the PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultTooLarge"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The converter failed or returned something that is not a valid document (`worker_unreachable`, `worker_invalid_response`), or, with `format` `html` or `both`, the document could not be rendered as HTML within its time and memory limits (`render_failed`). The pages held for the request are released. For `render_failed`, ask for `format=markdown`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "worker_unreachable",
                  "message": "The converter failed before returning a document."
                }
              }
            }
          },
          "503": {
            "description": "Large-file OCR or its page/session store is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getStanding",
        "summary": "Read your own allowance",
        "description": "Returns whether you call anonymously or with a key, your ceiling, what is left and when it resets. Reading it uses none of your allowance, so check here before you retry after a timeout.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Your standing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Standing"
                },
                "example": {
                  "product": "pdfmd",
                  "caller": "anon",
                  "limitKind": "pages_day",
                  "unit": "pages",
                  "limit": 25,
                  "remaining": 10,
                  "used": 15,
                  "maxPagesPerFile": 25,
                  "reset": 1790000000,
                  "resetIso": "2026-10-09T00:00:00.000Z",
                  "backend": "upstash",
                  "persistent": true,
                  "unavailable": false,
                  "recover": "https://pdfmd.dev/billing/regenerate",
                  "pricing": "https://pdfmd.dev/pricing",
                  "free_key": "https://pdfmd.dev/billing/free"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your pdfmd.dev key. Optional. Without it the request is anonymous. Metered routes reject an unknown or revoked key with 401. Remove the header to use the anonymous tier."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same key, sent as `Authorization: Bearer YOUR_KEY`. Use one of the two headers, not both."
      }
    },
    "headers": {
      "XRateLimitLimit": {
        "description": "The ceiling that applies to this caller, in pages.",
        "schema": {
          "type": "integer",
          "examples": [
            25
          ]
        }
      },
      "XRateLimitRemaining": {
        "description": "Pages left in the current period.",
        "schema": {
          "type": "integer",
          "examples": [
            24
          ]
        }
      },
      "XRateLimitReset": {
        "description": "When the count resets, in Unix seconds.",
        "schema": {
          "type": "integer",
          "examples": [
            1790000000
          ]
        }
      },
      "XRateLimitUnit": {
        "description": "The unit of the three headers above: calls or pages.",
        "schema": {
          "type": "string",
          "enum": [
            "calls",
            "pages"
          ]
        }
      },
      "XCaller": {
        "description": "anon when the request had no valid key, key when it had one.",
        "schema": {
          "type": "string",
          "enum": [
            "anon",
            "key"
          ]
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before the next try.",
        "schema": {
          "type": "integer",
          "examples": [
            5
          ]
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The body of every error answer. `error` is a stable code. `message` is for a person.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "missing_file"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "RateLimitError": {
        "type": "object",
        "description": "The body of a 413 or 429 from the gate.",
        "required": [
          "error",
          "message",
          "pricing",
          "recover"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limited",
              "file_too_long"
            ]
          },
          "message": {
            "type": "string",
            "examples": [
              "Daily limit of 25 pages reached. It resets at 00:00 UTC."
            ]
          },
          "free_key": {
            "type": "string",
            "format": "uri",
            "description": "Only for a caller without a key: where to get a free key.",
            "examples": [
              "https://pdfmd.dev/billing/free"
            ]
          },
          "pricing": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://pdfmd.dev/pricing"
            ]
          },
          "recover": {
            "type": "string",
            "format": "uri",
            "description": "Where a subscriber recovers a lost key.",
            "examples": [
              "https://pdfmd.dev/billing/regenerate"
            ]
          },
          "pages": {
            "type": "integer",
            "description": "413 and some 429 answers: the page count the request needs."
          },
          "remaining": {
            "type": "integer",
            "description": "Some 429 answers: pages left."
          },
          "maxPagesPerFile": {
            "type": "integer",
            "description": "413: the longest file this caller may convert."
          }
        }
      },
      "Standing": {
        "type": "object",
        "description": "A caller's own standing. Reading it uses none of the allowance.",
        "required": [
          "product",
          "caller",
          "limitKind",
          "unit",
          "limit",
          "remaining",
          "used",
          "reset",
          "resetIso",
          "persistent",
          "unavailable",
          "recover",
          "pricing"
        ],
        "properties": {
          "product": {
            "type": "string",
            "enum": [
              "pdfmd",
              "filings"
            ]
          },
          "caller": {
            "type": "string",
            "enum": [
              "anon",
              "key"
            ]
          },
          "label": {
            "type": "string",
            "description": "The plan name for a key."
          },
          "plan": {
            "type": "string",
            "description": "The plan lookup key for a key, for example pdfmd_pro_monthly."
          },
          "limitKind": {
            "type": "string",
            "enum": [
              "calls_day",
              "pages_day",
              "pages_month"
            ]
          },
          "unit": {
            "type": "string",
            "enum": [
              "calls",
              "pages"
            ]
          },
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "used": {
            "type": "integer"
          },
          "maxPagesPerFile": {
            "type": "integer",
            "description": "Free tiers of pdfmd: the longest file one request may convert."
          },
          "reset": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "resetIso": {
            "type": "string",
            "format": "date-time"
          },
          "backend": {
            "type": "string",
            "enum": [
              "upstash",
              "memory"
            ]
          },
          "persistent": {
            "type": "boolean",
            "description": "True when the count is in shared storage. False means it is held per instance and a deploy resets it."
          },
          "unavailable": {
            "type": "boolean",
            "description": "True when a key was sent and the store could not confirm it."
          },
          "recover": {
            "type": "string",
            "format": "uri"
          },
          "pricing": {
            "type": "string",
            "format": "uri"
          },
          "free_key": {
            "type": "string",
            "format": "uri",
            "description": "Only for a caller without a key."
          }
        }
      },
      "ConvertUrlRequest": {
        "type": "object",
        "description": "Convert a PDF at a public link. The JSON body is at most 64 KB.",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https?://",
            "description": "A public http or https link to a PDF, up to 50 MB.",
            "examples": [
              "https://arxiv.org/pdf/1706.03762"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "markdown",
              "html",
              "both"
            ],
            "default": "markdown",
            "description": "Which text of the document to return. `markdown` (the default) returns `markdown`. `html` returns the same document as an HTML fragment in `html` and no `markdown`. `both` returns both fields. The multipart field or JSON field wins over a `?format=` query parameter of the same name. Any other value is a 400 `bad_format`. A caller without a key or with a free key whose HTML renders fail 5 times in a UTC day gets 429 `render_limit` for `html` and `both` until the next day. Paid plans are not limited this way."
          }
        }
      },
      "OcrUrlRequest": {
        "type": "object",
        "description": "OCR a PDF at a public link.",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https?://",
            "examples": [
              "https://example.org/scan.pdf"
            ]
          },
          "output": {
            "type": "string",
            "enum": [
              "markdown",
              "searchable_pdf"
            ],
            "default": "markdown"
          },
          "format": {
            "type": "string",
            "enum": [
              "markdown",
              "html",
              "both"
            ],
            "default": "markdown",
            "description": "Which text of the document to return. `markdown` (the default) returns `markdown`. `html` returns the same document as an HTML fragment in `html` and no `markdown`. `both` returns both fields. Applies when `output` is `markdown`; `searchable_pdf` ignores it. The multipart field or JSON field wins over a `?format=` query parameter of the same name. Any other value is a 400 `bad_format`. A caller without a key or with a free key whose HTML renders fail 5 times in a UTC day gets 429 `render_limit` for `html` and `both` until the next day. Paid plans are not limited this way."
          }
        }
      },
      "Warning": {
        "type": "object",
        "description": "A value the parser could not settle. The same message closes its page in the Markdown as an HTML comment.",
        "required": [
          "page",
          "kind",
          "message"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "kind": {
            "type": "string",
            "enum": [
              "raised_mark_unresolved",
              "table_value_not_in_source"
            ]
          },
          "message": {
            "type": "string"
          },
          "values": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "table_value_not_in_source: the values that the page does not print."
          }
        },
        "additionalProperties": true
      },
      "ConvertResult": {
        "type": "object",
        "description": "A converted document. Counted in pages, one page credit per page, whatever the format. Depending on `format`, `markdown`, `html` or both are present: always at least one of them.",
        "required": [
          "id",
          "parserVersion",
          "title",
          "pages",
          "scannedPages",
          "tables",
          "chars",
          "ms",
          "pageBreaks"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "A prefix of the SHA-256 of the file."
          },
          "parserVersion": {
            "type": "string",
            "description": "The parser version that read the file."
          },
          "title": {
            "type": "string"
          },
          "pages": {
            "type": "integer",
            "minimum": 1,
            "maximum": 400
          },
          "scannedPages": {
            "type": "integer",
            "minimum": 0,
            "description": "Pages with no text layer. The Markdown endpoint counts them and does not read their image text. Use /api/v1/ocr for that."
          },
          "tables": {
            "type": "integer",
            "minimum": 0
          },
          "chars": {
            "type": "integer",
            "minimum": 0
          },
          "ms": {
            "type": "integer",
            "minimum": 0,
            "description": "Conversion time in milliseconds."
          },
          "pageBreaks": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Character offsets in the Markdown where each page starts. They stay when `format` is `html`, which does not return the Markdown they point into."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Warning"
            },
            "description": "Parser 0.1.2 and later. A response from an earlier parser has no `warnings` field."
          },
          "markdown": {
            "type": "string",
            "description": "The document as Markdown. Present when `format` is `markdown` (the default) or `both`."
          },
          "html": {
            "type": "string",
            "description": "The document as an HTML fragment with no `<html>` or `<body>`, ready to embed: GitHub-flavoured tables, headings, lists, code blocks and links. Scripts, event handlers, iframes, styles and `javascript:` links are removed. The warnings that close a page in the Markdown as comments are not in the HTML; read `warnings`. Present when `format` is `html` or `both`."
          }
        },
        "additionalProperties": true
      },
      "OcrResult": {
        "description": "A converted document plus the OCR fields. Returned when `output` is `markdown`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ConvertResult"
          },
          {
            "type": "object",
            "required": [
              "operation",
              "output",
              "language",
              "ocrPages"
            ],
            "properties": {
              "operation": {
                "type": "string",
                "enum": [
                  "ocr"
                ]
              },
              "output": {
                "type": "string",
                "enum": [
                  "markdown"
                ]
              },
              "language": {
                "type": "string",
                "enum": [
                  "eng"
                ],
                "description": "English printed text only."
              },
              "ocrPages": {
                "type": "integer",
                "minimum": 0,
                "description": "Image-only pages that OCR read."
              }
            }
          }
        ]
      },
      "TooLarge": {
        "type": "object",
        "description": "A 413 with no page count: the upload or the JSON body is over the size limit.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "too_large"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ResultTooLarge": {
        "type": "object",
        "description": "The converted result does not fit the response limit, or, with `format` `html` or `both`, its Markdown is too long to render as HTML. No pages are charged, except for an OCR request without a key or with a free key.",
        "required": [
          "error",
          "message",
          "resultBytes",
          "maxResultBytes"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "result_too_large"
            ]
          },
          "message": {
            "type": "string"
          },
          "resultBytes": {
            "type": "integer"
          },
          "maxResultBytes": {
            "type": "integer",
            "examples": [
              4000000
            ]
          }
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "The daily or monthly allowance is used up. Without a key the body names the free key. Reset time is in `X-RateLimit-Reset`. The count is in pages.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "X-RateLimit-Unit": {
            "$ref": "#/components/headers/XRateLimitUnit"
          },
          "X-Caller": {
            "$ref": "#/components/headers/XCaller"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitError"
            },
            "example": {
              "error": "rate_limited",
              "message": "Daily limit of 25 pages reached. It resets at 00:00 UTC.",
              "free_key": "https://pdfmd.dev/billing/free",
              "pricing": "https://pdfmd.dev/pricing",
              "recover": "https://pdfmd.dev/billing/regenerate"
            }
          }
        }
      },
      "TrialEnded": {
        "description": "A trial key has ended. Choose a plan to keep using the API.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "message",
                "pricing"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "trial_ended"
                  ]
                },
                "message": {
                  "type": "string"
                },
                "pricing": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "example": {
              "error": "trial_ended",
              "message": "This trial key has ended. Choose a plan to keep using the API.",
              "pricing": "https://pdfmd.dev/pricing"
            }
          }
        }
      },
      "InvalidKey": {
        "description": "The supplied key is invalid or inactive. Check the key or remove the authentication header to use the anonymous tier.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_api_key",
              "message": "The API key is not valid for this service."
            }
          }
        }
      }
    }
  }
}