A one-off conversion is easy. You drag a PDF into a converter, wait a few seconds, and copy the Markdown. But the moment conversion becomes part of a product, that manual flow breaks down. Users upload files you never see. Documents arrive at 3am. A single customer might send a hundred PDFs in a batch. When you need conversion to happen reliably, on demand, and without a human in the loop, you need an API.
This guide walks through designing a PDF to Markdown integration from the ground up: how to structure the request and response, how to handle files at scale without blocking your app, how to deal with the failures that will inevitably happen, and how to ship the whole thing to production with confidence. The examples are practical and language-agnostic, so you can adapt them to whatever stack you run.
When You Need an API Instead of a Script
A client-side script or a local library is often the right tool. If you are converting your own files on your own machine, reach for something like a Python conversion script and move on. An API earns its place when one or more of the following is true:
- Untrusted input: Your users upload the PDFs, so you cannot predict the size, format, or quality. You need a service that handles scanned pages, tables, and multi-column layouts consistently.
- Server-side workflows: The conversion happens as part of a pipeline. A file lands in storage, a webhook fires, or a scheduled job runs, and no browser is involved.
- Scale: You process more files than a single machine can comfortably handle, or you need conversions to run in parallel.
- Consistency: You want the same output quality every time, without maintaining and patching a stack of parsing libraries yourself.
If any of those describe your situation, a hosted conversion endpoint saves you from owning a fragile part of your infrastructure. You send bytes, you get clean Markdown back.
Designing the Integration
The Basic Request Shape
Almost every conversion API follows the same pattern: you POST the file, you authenticate with an API key, and you receive Markdown in the response body or a link to it. Here is the simplest possible call using curl:
curl -X POST https://api.pdftomd.cloud/v1/convert \
-H "Authorization: Bearer ${API_KEY}" \
-F "file=@invoice.pdf" \
-o invoice.mdTwo details matter here. First, the file is sent as multipart/form-data, which is the standard way to upload binary content over HTTP. Second, the API key travels in the Authorization header, never in the URL, so it does not leak into logs or browser history.
The Response
A well-designed conversion endpoint returns structured JSON so you can handle both success and failure programmatically. A typical successful response looks like this:
{
"status": "completed",
"markdown": "# Invoice 2043\n\n| Item | Qty | Price |\n| ... |",
"pages": 3,
"characters": 4820
}Design your code around the status field rather than the HTTP status code alone. A request can return 200 OK at the transport layer while the conversion itself failed, for example when a PDF is password protected or corrupt. Reading an explicit status field keeps your logic clear.
Synchronous vs Asynchronous
Small files convert in a second or two, so a synchronous request that blocks until the Markdown is ready is perfectly fine. Large documents are different. A 300-page report can take longer than a typical HTTP timeout allows, so a synchronous call will hang and eventually fail.
For anything beyond a few pages, prefer an asynchronous pattern. You submit the file and immediately get back a job ID. You then either poll a status endpoint or, better, register a webhook that fires when the conversion finishes. Polling is simpler to build; webhooks are more efficient and scale better. If you expect a mix of small and large files, support both: convert small files inline and queue large ones.
Handling Files at Scale
Never Block Your Request Thread
The most common mistake in a first integration is converting a file inside the same request that a user is waiting on. If a customer uploads a large PDF and your web handler waits for the conversion to finish before responding, you tie up a worker, risk a timeout, and give the user a spinner that might never resolve.
Instead, accept the upload, store the file, enqueue a background job, and return immediately. The job does the conversion and updates a record when it is done. Your frontend polls that record or subscribes to a real-time channel. This keeps your web tier responsive no matter how large the incoming files are.
Concurrency and Rate Limits
When you process a batch, resist the urge to fire every request at once. Most APIs enforce rate limits, and slamming the endpoint with a thousand simultaneous calls will earn you a wall of 429 Too Many Requests responses. Use a worker pool with a fixed concurrency, for example five to ten in-flight conversions at a time, and let a queue feed it. Here is the idea in pseudocode:
const queue = [...pendingFiles]
const CONCURRENCY = 8
async function worker() {
while (queue.length > 0) {
const file = queue.shift()
await convertWithRetry(file)
}
}
await Promise.all(
Array.from({ length: CONCURRENCY }, () => worker())
)This gives you predictable throughput, keeps you inside the rate limit, and makes your resource usage easy to reason about.
Idempotency
Networks fail and jobs get retried. If your batch process crashes halfway through and restarts, you do not want to convert the first half of the files a second time and pay for them twice. Give each conversion an idempotency key, usually a hash of the file contents, and check whether you already have a result for that key before submitting. This makes retries safe and keeps your bill honest.
Error Handling That Survives Production
In a demo, everything works. In production, files are corrupt, networks drop, and rate limits bite. A robust integration plans for all three. Group the failures you will see into a few buckets and handle each deliberately:
- Transient errors (timeouts, 429, 503): retry with exponential backoff and a jitter, up to a sensible cap. These usually resolve on their own.
- Permanent errors (400 bad request, password-protected file, unsupported format): do not retry. Surface a clear message to the user and log the reason.
- Partial failures in a batch: track each file independently so one bad PDF does not sink the whole job. Record which files succeeded and which need attention.
A simple retry wrapper covers most of these:
async function convertWithRetry(file, attempt = 1) {
try {
return await convert(file)
} catch (err) {
if (isPermanent(err) || attempt >= 4) throw err
const waitMs = 2 ** attempt * 250
await sleep(waitMs)
return convertWithRetry(file, attempt + 1)
}
}Always log enough context to debug a failure without the original file in hand: the file name, its size, the response status, and the job ID. When a customer reports that their document did not convert, that log line is what lets you answer them in minutes instead of hours.
Security Considerations
You are handling other people's documents, some of which will contain sensitive data. Treat the integration with the same care you give any part of your app that touches user files.
- Keep API keys server-side. Never embed a conversion key in client-side JavaScript or a mobile app, where anyone can extract it. Proxy conversion requests through your own backend.
- Validate uploads. Check the file type and size before you forward anything. Reject files that are not actually PDFs and cap the maximum size so a single upload cannot exhaust your resources.
- Understand data retention. Know how long the conversion service stores files and results, and make sure that policy matches what you promise your own users.
- Use HTTPS everywhere. This is table stakes, but it is worth stating: every hop that carries a document or an API key must be encrypted in transit.
A Complete Flow, End to End
Putting the pieces together, here is what a production-grade integration looks like when a user uploads a PDF to your app:
- Your frontend uploads the PDF to your backend, which validates the type and size.
- Your backend stores the file, creates a conversion record with status
pending, and enqueues a background job. It returns the record ID to the frontend right away. - A worker picks up the job, computes an idempotency key, and calls the conversion API with a retry wrapper.
- On success, the worker saves the Markdown, updates the record to
completed, and stores metadata like page count. - Your frontend, which has been polling or listening on a real-time channel, sees the record flip to
completedand renders the Markdown. - On permanent failure, the record moves to
failedwith a human-readable reason, and the user sees a clear message.
This flow stays responsive under load, survives restarts, and gives every file a clear lifecycle you can inspect. Once you have clean Markdown flowing through your app, you can feed it into downstream systems: index it for search, drop it into RAG pipelines for AI features, or store it as version-controlled documentation. See more patterns on our use cases page.
Testing Before You Ship
Do not wait for real users to find the edge cases. Build a small corpus of test PDFs that covers the range you expect to see: a clean text document, a scanned page, one heavy with tables, a multi-column layout, a corrupt file, and a password-protected one. Run your integration against all of them and assert on the output. This suite catches regressions when you change providers, upgrade a dependency, or refactor the pipeline.
It also gives you a baseline for quality. When you evaluate conversion services, running the same corpus through each one tells you far more than a marketing page. If you are still choosing a tool, our roundup of the best PDF to Markdown converters is a good starting point.
Frequently Asked Questions
Do I need an API, or is a library enough?
If you convert your own files on your own machine, a library or script is simpler and cheaper. Reach for an API when the files come from users, when conversion runs server-side without a browser, when you process at scale, or when you want consistent quality without maintaining parsing code yourself.
How do I handle very large PDFs?
Use an asynchronous pattern. Submit the file, get a job ID, and either poll a status endpoint or register a webhook that fires on completion. Never convert a large file inside the same request a user is waiting on, or you risk timeouts and tied-up workers.
What happens when a conversion fails?
Separate transient failures from permanent ones. Retry timeouts and rate-limit responses with exponential backoff. Do not retry bad requests, corrupt files, or password-protected documents; surface a clear message instead. In a batch, track each file independently so one failure does not sink the whole job.
How do I keep my API key safe?
Keep it on your server and proxy conversion requests through your own backend. Never ship a key in client-side JavaScript or a mobile app, where it can be extracted. Send the key in the Authorization header, not in the URL, so it does not end up in logs.
Can I try conversion before wiring up an API?
Yes. The free tier at pdftomd.cloud gives you three conversions a month with no credit card, so you can see the Markdown quality on your own documents before you write a line of integration code.
The Takeaway
A conversion API turns PDF to Markdown from a manual chore into a reliable building block inside your product. The winning integration is not the one with the cleverest code. It is the one that stays responsive under load, retries the failures worth retrying, refuses the ones that are not, and keeps your users' documents safe. Design around an explicit status, push conversion into background workers, respect rate limits with a bounded queue, and test against a corpus that looks like the real world.
Get those fundamentals right and conversion becomes something you never think about again: files go in, clean Markdown comes out, and your app moves on to the work that actually differentiates it.
