Convert PDF to Markdown with cURL
No programming language required. With curl and jq you can convert a PDF to Markdown straight from the terminal, a Makefile, a CI step or a cron job. Copy the recipe below, set your key, and run it.
Three curls and a poll loop
curl -X POST the PDF to /api/v2/jobs with your key, pull job_id out with jq, loop on /api/v2/jobs/{job_id} until the status is ready, then curl /api/v2/jobs/{job_id}/download with -o to save the Markdown. It is pure HTTP, so it runs anywhere a shell does, with no SDK and nothing to compile.
The full shell recipe
Save as convert.sh, chmod +x, and run it. Swap in your key and PDF.
#!/usr/bin/env bash set -euo pipefail API="https://pdf2md.dev/api/v2" AUTH="Authorization: Bearer p2m_your_key" # 1) create a job from a PDF URL # (to upload a local file instead, use: -F [email protected] ) JID=$(curl -fsS -X POST "$API/jobs" -H "$AUTH" \ -H "Content-Type: application/json" -H "Idempotency-Key: report-2026-01" \ -d '{"url":"https://example.com/report.pdf"}' | jq -r .job_id) # 2) poll until ready or error while :; do JOB=$(curl -fsS "$API/jobs/$JID" -H "$AUTH") STATUS=$(echo "$JOB" | jq -r .status) [ "$STATUS" = "ready" ] && break if [ "$STATUS" = "error" ]; then echo "failed: $(echo "$JOB" | jq -r .error_code)" >&2 exit 1 fi sleep 3 done # 3) download the Markdown curl -fsS "$API/jobs/$JID/download" -H "$AUTH" -o report.md echo "saved report.md"
The -f flag makes curl fail on HTTP errors, and set -euo pipefail stops the script on the first problem, so a broken run never silently writes an empty file.
Built for automation
Because it is a single self-contained script with a real exit code, the recipe drops into the places a full program would be overkill.
CI pipelines
Add it as a build step to turn documentation PDFs into Markdown on every commit. A failed conversion fails the step.
Cron jobs
Schedule it to pull and convert a nightly report, writing the Markdown where your docs site or pipeline expects it.
Secrets, not literals
Keep the API key in a CI secret or an environment variable and reference it in the Authorization header, never hard-coded.
Want it inside an app instead? See the Python, Node.js and Go tutorials.
The whole API in one page
Every endpoint, field and error code is on the developer hub, with the OpenAPI spec you can import into Postman or generate a client from.
Other shells and webhooks
The recipe is bash, but the three calls are just HTTP, so they port anywhere curl runs.
Windows and PowerShell
curl ships with modern Windows, so the same calls work in PowerShell or a .cmd file; you only adjust the quoting (PowerShell prefers single quotes around the JSON, or use Invoke-RestMethod with a hashtable body). In a Makefile, a Dockerfile build step or a GitHub Actions run: block, the script is unchanged.
Skip the poll loop
Polling is the simplest approach and is fine for one file or a handful. For many files, or to avoid the wait entirely, register a webhook and the API calls you back when a job is ready, so a script can fire off conversions and process results as they arrive instead of looping.
Whatever the shell, keep the key out of the script body: read it from an environment variable or a CI secret. And if you need to inspect a result before saving, pipe the download into a viewer (curl ... | less) instead of -o, since the endpoint returns plain Markdown text.
Common questions
What do I need to run the cURL recipe?
Just curl and jq. curl makes the HTTP calls and jq reads job_id and status out of the JSON responses. Both are available on most systems or a single package install away.
How do I convert a local file with curl?
Use a multipart upload: curl -X POST /api/v2/jobs -F [email protected] with the Authorization header, instead of the JSON -d body that carries a url.
How do I use this in CI or a cron job?
The script exits non-zero when a job fails, so it slots into a CI step or a scheduled cron job. Store the API key as a CI secret or an environment variable and pass it in the Authorization header.
Can I do it without jq?
jq is the cleanest way to read job_id and status, but you can also parse the JSON with grep and sed if jq is not available. jq is recommended for reliability.
What does the Idempotency-Key header do?
Sending the same Idempotency-Key on a retried create returns the same job instead of starting a duplicate conversion, which is useful when a flaky CI step reruns.