Convert PDF to Markdown in Node.js
Three calls and you are done: create a job, poll until it is ready, then download clean Markdown. The example below is plain Node 18+ with the global fetch – no dependencies, no GPU, just the REST API.
Create, poll, download
The PDF to Markdown API is a small job API. You POST a PDF (a URL or an uploaded file) to /api/v2/jobs with your API key, get back a job_id, poll /api/v2/jobs/{job_id} until the status is ready, then download the Markdown from /api/v2/jobs/{job_id}/download. OCR, tables and formulas are handled server-side, so there is nothing to install in your Node app.
The full example
Save as convert.mjs and run node convert.mjs. Replace the key and the PDF URL.
// Node 18+ has a global fetch, so no dependencies are needed.
const API = "https://pdf2md.dev/api/v2";
const H = { Authorization: "Bearer p2m_your_key" };
// 1) create a job from a PDF URL
const created = await fetch(`${API}/jobs`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json", "Idempotency-Key": "report-2026-01" },
body: JSON.stringify({ url: "https://example.com/report.pdf" }),
});
if (!created.ok) throw new Error(`create failed: ${created.status}`);
const { job_id } = await created.json();
// 2) poll until ready or error
let job;
do {
await new Promise((r) => setTimeout(r, 3000));
job = await (await fetch(`${API}/jobs/${job_id}`, { headers: H })).json();
} while (!["ready", "error"].includes(job.status));
if (job.status === "error") {
throw new Error(`conversion failed: ${job.error_code} ${job.error_message}`);
}
// 3) download the Markdown
const md = await (await fetch(`${API}/jobs/${job_id}/download`, { headers: H })).text();
if (job.truncated) console.warn("note: partial result (hit the time budget)");
const fs = await import("node:fs/promises");
await fs.writeFile("report.md", md);
console.log("saved report.md");
The Idempotency-Key makes a retried create safe: if the same key is sent twice, you get the same job back instead of a duplicate conversion.
What each call does
Create a job
POST /api/v2/jobs with the API key as a Bearer token and a JSON body of { "url": "..." }. To convert a local file instead, send multipart/form-data with a file field. The response gives a job_id.
Poll until ready
GET /api/v2/jobs/{job_id} returns a status of queued, processing, ready or error. Poll every few seconds until it settles.
Download the Markdown
GET /api/v2/jobs/{job_id}/download returns the Markdown text. Write it to a .md file, hand it to an LLM, or store it for a RAG pipeline.
Errors, retries and big files
Handle failures
processing_timeout, conversion_failed); error_message is safe to log.ready job means a partial result that hit the time budget on a very long document.Scale it up
To convert many PDFs, run the same three calls per file with a small concurrency limit, or switch from polling to webhooks so you are notified when each job is ready. The contract is identical in TypeScript; type the job response and you are set.
Prefer Python or the shell? See the Python tutorial and the cURL recipe.
Building an agent or pipeline?
The same conversion is available as a hosted MCP endpoint, so an AI agent can convert PDFs with no setup. See the developer hub for the full API reference and the OpenAPI spec.
Common questions
Do I need an API key to convert PDFs in Node.js?
Yes, for the REST API you pass an API key as a Bearer token. You can also convert anonymously in the browser or the web app without a key; the API key is for programmatic and automated use.
Which Node.js version do I need?
Node 18 or newer, which ships a global fetch, so the example needs no extra dependencies. On older versions, install a fetch polyfill or use a request library.
How do I convert a local PDF file instead of a URL?
Send a multipart/form-data POST to /api/v2/jobs with the file field set to the PDF, using FormData and a Blob, instead of a JSON body with a url. The rest of the flow (poll, download) is the same.
How do I handle errors and timeouts?
When status is error, read error_code (for example processing_timeout or conversion_failed) and error_message. A long document can finish ready with truncated set to true, meaning a partial result that hit the time budget.
Can I use webhooks instead of polling?
Yes. Polling is simplest to start, but the API also supports webhooks so you are notified when a job is ready instead of polling. See the developer hub for the webhook setup.