pdf2md API
PDF to Markdown. Version 0.3.6.
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
| Option | Meaning |
|---|---|
pages | 1-indexed selection: 1,3,5-7 |
password | for an encrypted document |
profile | fidelity (default) or compact, which spends fewer tokens |
includePageMarkers | insert <!-- Page N --> between pages |
includeImages, includeLinks | emit image placeholders / extracted hyperlinks |
stripHeadersFooters | drop running headers and footers |
detectHeaders, detectLists, detectCode, detectBold, detectItalic | turn 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, fixHyphenation | post-processing, all on by default |
baseFontSize | body text size in points, when the heuristic gets it wrong |
textOnImagePages | on 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. A client that shows MCP Apps also gets open_converter, which
puts the converter into the conversation: the PDF is converted in that frame, in the browser, and
reaches the model only if the person sends the result to the chat. 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. A client set up with a header rather than a sign-in may send an API key instead, as
X-Api-Key — one made for pdf2md; a key for any other product is refused.
Response headers
| Header | Meaning |
|---|---|
X-Pdf-Type | TextBased, Scanned, ImageBased or Mixed |
X-Pdf-Pages | page count |
X-Pdf-Images | image files in the answer, when images were asked for |
X-Pdf-Needs-Ocr | comma-separated 1-indexed pages, empty when none |
X-Pdf-Encoding-Issues | true when the text is probably garbled |
X-Pdf-Ms | conversion time |
Status codes
401 | missing or wrong API key, or the wrong password for an encrypted PDF |
413 | larger than 32 MB, or more than 300 pages |
415 | not a PDF |
422 | a PDF, but nothing could be extracted — a scan, usually |
429 | rate limit or daily quota |
503 | the queue is full; retry shortly |
504 | the 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.