PDF to Markdown (pdf2md)

Turn PDFs into Markdown, find which pages are scans, pull out images. Nothing is stored.

Documentation

PDF to Markdown. Version 0.3.2.

This API needs an account. Sign in or create one, make an API key for pdf2md there, and send it as X-Api-Key: ak_… (or an access token as Authorization: Bearer). Every account starts with a free allowance of conversions; GET /api/v1/usage says what is left, and so do the X-Usage-* headers on each answer — X-Usage-Reset among them on a monthly plan, the moment its counter starts over. A used-up plan answers 402 with where to upgrade.

Everything is parsed in process by a WebAssembly module. Uploads are held in memory for the length of the request and are never written to disk or sent anywhere else.

Convert

curl -X POST https://pdf2md-api.dudko.dev/api/v1/convert \
  -F [email protected] \
  -F profile=compact

Returns text/markdown. Add ?format=json for the full result — page count, detected type, layout, which pages would need OCR.

With the pictures: ?format=zip answers application/zip holding <name>.md and assets/img-1.png, assets/img-2.jpg, … exactly as the Markdown links them. ?format=json&images=true carries the same files base64 in assets. JPEGs pass through as they are; everything else — Flate and LZW samples, CCITT fax, JBIG2, JPEG 2000 — is written as PNG (jpx=keep keeps a JPEG 2000 as the .jp2 it is). Inline images and the pictures in annotations — stamps, signatures, form fields — have no place in the text, so they are linked at the end of their page. So are the vector figures: charts, diagrams and logos drawn with path operators come out as SVG, with the page's text inside them as <text> (vectors=false leaves them out). skipped lists what could not be turned into a file and why, and the Markdown says so where the picture was. X-Pdf-Images counts the files.

The body may also be a raw application/pdf, or JSON {"pdf": "<base64>", "options": {…}}. Options work as query parameters in every case.

Try it

From this page, against this instance. The file goes to the endpoint below and nowhere else; nothing is kept after the response.

Detect

curl -X POST https://pdf2md-api.dudko.dev/api/v1/detect --data-binary @report.pdf

Classification only, in milliseconds: is this text, a scan or a mix, how many pages, and which of them would need OCR. Use it to route work before paying for a conversion.

Plain text

curl -X POST https://pdf2md-api.dudko.dev/api/v1/text --data-binary @report.pdf

Options

OptionMeaning
pages1-indexed selection: 1,3,5-7
passwordfor an encrypted document
profilefidelity (default) or compact, which spends fewer tokens
includePageMarkersinsert <!-- Page N --> between pages
includeImages, includeLinksemit image placeholders / extracted hyperlinks
stripHeadersFootersdrop running headers and footers
detectHeaders, detectLists, detectCode, detectBold, detectItalicturn individual detectors off with =false
detectUnderline=true wraps text with a line under it in <u>; off by default, because rules and form lines pass for underlining far more often than underlining occurs
removePageNumbers, formatUrls, fixHyphenationpost-processing, all on by default
baseFontSizebody text size in points, when the heuristic gets it wrong
textOnImagePageson by default: a page the detector calls a scan still gives up the text it carries (a caption under a full-page figure); =false to get no Markdown for such documents

GET /api/v1/options returns the same list as JSON, plus this instance's limits.

For an assistant

An MCP endpoint at https://pdf2md-api.dudko.dev/mcp is the front door to this API for an assistant: api_access says how the API works, and api_request checks the options and returns the exact request with a short-lived credential for the signed-in account. No document passes through it. Point an MCP client at that address; it will find the rest — which authorization server issues the tokens, and what scope to ask for — at https://pdf2md-api.dudko.dev/.well-known/oauth-protected-resource/mcp. A token is accepted only if it was issued for this address and no other.

Response headers

HeaderMeaning
X-Pdf-TypeTextBased, Scanned, ImageBased or Mixed
X-Pdf-Pagespage count
X-Pdf-Imagesimage files in the answer, when images were asked for
X-Pdf-Needs-Ocrcomma-separated 1-indexed pages, empty when none
X-Pdf-Encoding-Issuestrue when the text is probably garbled
X-Pdf-Msconversion time

Status codes

401missing or wrong API key, or the wrong password for an encrypted PDF
413larger than 32 MB, or more than 300 pages
415not a PDF
422a PDF, but nothing could be extracted — a scan, usually
429rate limit or daily quota
503the queue is full; retry shortly
504the conversion hit the deadline

This is not OCR

A scanned document is reported as Scanned with no Markdown and a 422 from /convert. Run it through an OCR tool first. Watch X-Pdf-Encoding-Issues as well: text came out, but the fonts declare an encoding the bytes do not honour, so the characters are probably wrong.

Doing it yourself

The same conversion runs in a browser and in Node with no server at all — npm i @dudko.dev/pdf-to-md-core, or npx @dudko.dev/pdf-to-md-cli file.pdf. The web app at pdf2md.dudko.dev never calls this API: it runs the same module in the visitor's browser.