MarqPDF API
Compress, convert, OCR, merge and protect PDFs from your own code. Upload a file, run an operation on it, download the result: three HTTP calls.
Quick start
Create a key under Account → Developers, then:
export MARQPDF_API_KEY=mp_live_…
# 1. Upload
curl -s https://marqpdf.com/v1/files \
-H "Authorization: Bearer $MARQPDF_API_KEY" \
-F file=@contract.pdf
# {"object":"file","id":"file_8rX2…","name":"contract.pdf","pages":12,…}
# 2. Compress it to fit under 1 MB
curl -s https://marqpdf.com/v1/compress \
-H "Authorization: Bearer $MARQPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file": "file_8rX2…", "target_mb": 1}'
# {"object":"job","status":"succeeded",
# "output":{"files":[{"id":"file_Qm7…","name":"contract-compressed.pdf",
# "size":948211,"download_url":"https://marqpdf.com/v1/files/file_Qm7…/content?e=…&t=…"}],
# "note":"…"}}
# 3. Download
curl -s -o small.pdf "https://marqpdf.com/v1/files/file_Qm7…/content" \
-H "Authorization: Bearer $MARQPDF_API_KEY"
Operations take file ids and return new files, so steps chain: OCR a scan, then compress the searchable PDF, then protect it.
SDKs
Both take a local path, a URL, bytes or a file id wherever a file goes, wait for the job, and raise a typed error with the API's code if it fails. No dependencies.
JavaScript (Node 18+, Deno, Bun)
npm install @nucleushq/marqpdf
import { MarqPDF } from "@nucleushq/marqpdf";
const marq = new MarqPDF(); // reads MARQPDF_API_KEY
const job = await marq.compress("contract.pdf", { target_mb: 1 });
await marq.download(job, "contract-small.pdf");
const scan = await marq.ocr("scan.pdf"); // steps chain on outputs
const doc = await marq.convert(scan.output.files[0], { to: "docx" });
Python (3.8+)
pip install https://marqpdf.com/sdk/nucleushq_marqpdf-0.3.0-py3-none-any.whl
from marqpdf import MarqPDF
marq = MarqPDF() # reads MARQPDF_API_KEY
job = marq.compress("contract.pdf", target_mb=1)
marq.download(job, "contract-small.pdf")
print(marq.extract_text("https://example.com/statement.pdf")["output"]["text"])
Each SDK also has every operation (merge, split, protect, …), file and job calls, usage(), and a webhook signature check. For any other language, generate a client from the OpenAPI spec.
AI agents (MCP)
MarqPDF is an MCP server, so an AI agent can use it as a set of tools: compress_pdf, ocr_pdf, convert_file, merge_pdfs, extract_text and the rest. There are two ways to connect.
Files on your computer: the local server
For Claude Desktop, Claude Code, Cursor and other clients that run a command. Tools take file paths, and results are saved next to the originals (never over them unless you ask), so you can say "compress ~/Downloads/lease.pdf under 2 MB".
{
"mcpServers": {
"marqpdf": {
"command": "npx",
"args": ["-y", "@nucleushq/marqpdf-mcp"],
"env": { "MARQPDF_API_KEY": "mp_live_…" }
}
}
}
That goes in claude_desktop_config.json (Claude Desktop) or ~/.cursor/mcp.json (Cursor). For Claude Code:
claude mcp add marqpdf -e MARQPDF_API_KEY=mp_live_… -- npx -y @nucleushq/marqpdf-mcp
Agents in the cloud: the remote server
Streamable HTTP at https://marqpdf.com/v1/mcp, with your API key as a bearer token. Files come in by URL (or base64 for small ones) and go out as one-hour download_url links.
claude mcp add --transport http marqpdf https://marqpdf.com/v1/mcp --header "Authorization: Bearer mp_live_…"
// Cursor, VS Code and other clients that take a URL
{ "mcpServers": { "marqpdf": {
"url": "https://marqpdf.com/v1/mcp",
"headers": { "Authorization": "Bearer mp_live_…" } } } }
A tool call is an API call: the same files, limits and monthly operations.
AI: extract and ask
Two operations read a document with a language model. Both take a PDF or an image (a phone photo of an invoice works).
POST /v1/extract: fields as JSON
curl -s https://marqpdf.com/v1/extract \
-H "Authorization: Bearer $MARQPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file": "file_8rX2…",
"fields": ["invoice_number", "supplier", {"name": "total", "type": "number"}, {"name": "due_date", "type": "date"}]}'
# "output": {"data": {"invoice_number": "INV-2041", "supplier": "Northwind Design Studio",
# "total": 5136, "due_date": "2025-04-13"},
# "model": "gemini/gemini-2.5-flash", "files": [ …the same as a .json file… ]}
A field given as a plain name comes back as text. Field types are string (the default), number, integer, boolean, date (YYYY-MM-DD) and list. For nested data, pass a JSON Schema in schema instead. A field the document doesn't contain comes back null; values aren't guessed. Add instructions for anything else ("amounts in AED", "the buyer, not the seller").
POST /v1/ask: an answer from the document
curl -s https://marqpdf.com/v1/ask \
-H "Authorization: Bearer $MARQPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file": "file_lease…", "question": "When can the tenant end the lease early?"}'
# "output": {"text": "After 12 months, with 60 days' written notice (clause 14.2, page 6).", "model": "…"}
Answers come from the document only; if it doesn't say, the answer says so.
Workflows
A workflow is a named chain of operations you save once and run on any file. Each step works on the previous step's output; a step that only produces data (extract, ask) passes its inputs on.
curl -s https://marqpdf.com/v1/workflows \
-H "Authorization: Bearer $MARQPDF_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "Scan to small searchable PDF",
"steps": [{"operation": "ocr"},
{"operation": "compress", "options": {"target_mb": 1}},
{"operation": "protect", "options": {"password": "…"}}]}'
# {"object": "workflow", "id": "wf_9Kd…", …}
curl -s https://marqpdf.com/v1/workflows/wf_9Kd…/run \
-H "Authorization: Bearer $MARQPDF_API_KEY" -H "Content-Type: application/json" \
-d '{"file": "file_8rX2…"}'
# the job: output.files = the last step's files, output.steps = what each step did
Steps are checked when you save (up to 10 steps, 50 workflows an account), and the first step again against the real file when you run. Each step that succeeds counts as one operation. POST /v1/workflows/run with steps in the body runs a chain without saving it. You can also save workflows on the Developers screen, and agents can run them through MCP (list_workflows, run_workflow).
The agent
POST /v1/agent takes files and a goal in plain words. The agent, running on the model you pick for it, chooses MarqPDF operations, reads each result, and stops when the goal is met:
curl -s https://marqpdf.com/v1/agent \
-H "Authorization: Bearer $MARQPDF_API_KEY" -H "Content-Type: application/json" \
-d '{"files": ["file_jan…", "file_feb…", "file_mar…"],
"goal": "Make these scanned statements searchable, merge them in date order and get the result under 2 MB",
"model": "anthropic/claude-opus-5-5", "wait": 300}'
# output.text = its summary
# output.files = the files it hands back
# output.steps = every operation it ran, with arguments and results
It only sees the files you give it and the files it makes; it can't reach anything else on your account. max_steps (1–15, default 8) caps the model turns. Each operation it runs counts as one; on MarqPDF's model the run also uses one AI operation.
Your own model
AI operations, and the AI edits in MarqPDF's editor, run on MarqPDF's model unless you choose another. Add your own key under Account → Developers → Models for any of:
| Provider | Model ids, e.g. | Reads PDFs directly |
|---|---|---|
| Anthropic (Claude) | anthropic/claude-opus-5-5, anthropic/claude-sonnet-5-5 | Yes |
| OpenAI | openai/gpt-5.1 | Yes |
| Google Gemini | gemini/gemini-2.5-pro, gemini/gemini-2.5-flash | Yes |
| OpenRouter | openrouter/ + any of its model ids | Yes |
| Any OpenAI-compatible endpoint | custom/ + the model your endpoint serves (a public https base URL) | No: MarqPDF sends the text, OCR'd if it's a scan |
Then pick a model for each step (extraction, questions, the agent, the editor) on the same screen, or name one per request with "model": "anthropic/claude-opus-5-5". GET /v1/models lists what your account can use. Keys are checked with the provider when you add them, encrypted at rest, used only for your requests, and never shown again.
On your own key, the provider bills you and MarqPDF doesn't cap AI calls (they still count as operations). On MarqPDF's model, free accounts get 20 AI operations a month and paid plans 1,000.
Authentication
Send your key on every request as Authorization: Bearer mp_live_… (or X-API-Key: mp_live_…). Keys are shown once, when you create them, and stored only as a hash; if one leaks, revoke it under Account → Developers and it stops working at once. Keep keys on your server: a key in a web page or a mobile app is a key anyone can copy.
GET /v1 and GET /v1/openapi.json need no key.
Files
Upload: POST /v1/files
Send multipart/form-data with the file in a field named file (and optionally name), or JSON with a URL for MarqPDF to fetch:
curl -s https://marqpdf.com/v1/files \
-H "Authorization: Bearer $MARQPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/statement.pdf"}'
Up to 100 MB. PDFs, images (JPG, PNG, WebP, TIFF), Word, Excel and PowerPoint files, HTML, Markdown and text. A file's contents are checked against its name: a .pdf that isn't a PDF is refused. URLs must be http or https on a public address; private and internal addresses are refused.
The file object
{
"object": "file",
"id": "file_8rX2…",
"name": "contract.pdf",
"size": 2840113,
"content_type": "application/pdf",
"pages": 12,
"created_at": "2026-10-10T08:15:00Z",
"expires_at": "2026-10-11T08:15:00Z",
"source": "upload", // upload | url | job
"download_url": "https://marqpdf.com/v1/files/file_8rX2…/content?e=…&t=…"
}
download_url works without a key for one hour, so you can hand it to a browser, a customer or an AI agent. GET /v1/files/{id} returns a fresh one; GET /v1/files/{id}/content with your key always works while the file exists.
Other file calls
| Call | What it does |
|---|---|
GET /v1/files | Your files from the last 24 hours, newest first (?limit= up to 200). |
GET /v1/files/{id} | One file's details, with a fresh download_url. |
GET /v1/files/{id}/content | The file's bytes. |
DELETE /v1/files/{id} | Delete it now rather than after 24 hours. |
Operations
Every operation is POST /v1/{operation} with a JSON body naming its input as file (one file id) or files (a list), plus its options. Each one that succeeds counts as one operation.
| Operation | Input | Options | What it does |
|---|---|---|---|
POST /v1/compress | file | target_mb optionallevel optional (screen | ebook | printer | prepress) | Compress a PDF. Smallest file that fits under `target_mb`, or a fixed quality `level`. In the browser |
POST /v1/merge | files | — | Merge PDFs. Join 2–50 PDFs, in the order given. In the browser |
POST /v1/split | file | every optionalranges optional | Split a PDF. A ZIP of PDFs: every N pages, or the page ranges you give. In the browser |
POST /v1/rotate | file | angle (90 | 180 | 270)pages optional | Rotate pages. Counts as one operation when it succeeds. In the browser |
POST /v1/organize | file | pages | Reorder or drop pages. Keep `pages`, in that order. |
POST /v1/remove-pages | file | pages | Remove pages. Counts as one operation when it succeeds. |
POST /v1/page-numbers | file | position optional (bottom-center | bottom-right | bottom-left | top-center | top-right | top-left) | Add page numbers. Counts as one operation when it succeeds. |
POST /v1/watermark | file | text | Add a text watermark. Counts as one operation when it succeeds. In the browser |
POST /v1/protect | file | password | Password-protect a PDF. Counts as one operation when it succeeds. In the browser |
POST /v1/unlock | file | password optional | Remove a PDF's password. Needs the password unless the PDF only restricts printing or copying. In the browser |
POST /v1/ocr | file | — | Make a scanned PDF searchable. Adds a text layer (English and Arabic) under the page images. In the browser |
POST /v1/convert | file | to (pdf | docx | xlsx | pptx | jpg) | Convert a file. PDF → docx, xlsx, pptx or jpg (a ZIP of page images); images, Office files, HTML, Markdown or text → pdf. PDF → docx OCRs scans first. In the browser |
POST /v1/images-to-pdf | files | — | Images to one PDF. One page per image, in the order given. In the browser |
POST /v1/compare | files | — | Compare two PDFs. A PDF marking what changed between `files[0]` and `files[1]`. |
POST /v1/extract-text | file | ocr optional (auto | always | never) | Extract text. The PDF's text (also as a .txt output file). `ocr: auto` reads scans with OCR when there's no text layer. In the browser |
POST /v1/extract | file | fields optionalschema optionalinstructions optionalmodel optional | Extract data with AI. Read a PDF or an image (invoice, ID, statement, form) and return the fields you ask for as JSON in output.data (also saved as a .json output file). Name the fields, or pass a JSON Schema. A field the document doesn't contain comes back null. Runs on MarqPDF's model or one of your own (model). |
POST /v1/ask | file | questioninstructions optionalmodel optional | Ask a document. Answer a question about a PDF or an image, from the document only; the answer is in output.text. Runs on MarqPDF's model or one of your own (model). |
POST /v1/agent | files | goalmax_steps optionalmodel optional | Run the agent. Give a goal in plain words; the agent plans and runs MarqPDF operations on your files with the model you chose (pipeline "agent"), reading each result before the next step. output.text is its summary, output.files the files it hands back, output.steps every call it made. Each operation it runs counts as one. |
Two options work on every operation: wait (see jobs) and webhook_url (see webhooks). Requests are checked before anything runs: a wrong option, a missing file or an image sent to a PDF-only operation fails at once with a 400 that says what to change, and costs nothing.
Jobs and waiting
Every operation runs as a job. The request waits for it up to wait seconds (default 60, at most 300, 0 to not wait):
| Response | Meaning |
|---|---|
200 | Finished: the job, with output.files (and output.text for extract-text). |
202 | Still running. Poll GET /v1/jobs/{id} (also in the Location header) until status is succeeded or failed, or use a webhook. |
4xx / 5xx | Finished but failed: {"error": {…}, "job": {…}}. |
{
"object": "job",
"id": "job_3Fq…",
"operation": "ocr",
"status": "succeeded", // queued | running | succeeded | failed
"created_at": "…", "started_at": "…", "finished_at": "…",
"input": { "files": ["file_8rX2…"], "options": {} },
"output": { "files": [ { "object": "file", "id": "file_Hk1…", … } ] },
"error": null
}
GET /v1/jobs lists recent jobs. Jobs are kept for 7 days. A job interrupted by a server restart fails with interrupted and isn't counted; submit it again.
Webhooks
Add "webhook_url": "https://your.app/hooks/marqpdf" to any operation, and when the job finishes MarqPDF POSTs:
{ "type": "job.succeeded", // or job.failed
"created_at": "2026-10-10T08:16:02Z",
"data": { "object": "job", "id": "job_3Fq…", … } }
Answer with any 2xx. A delivery that fails is retried after 30 seconds and again after 5 minutes; the job's webhook field shows how it went. Redirects aren't followed, and the URL must be a public http or https address on port 80 or 443.
Verifying the signature
Each delivery has a MarqPDF-Signature: t=<unix time>,v1=<hex> header: an HMAC-SHA256, keyed with your webhook signing secret (Account → Developers), of the timestamp, a dot, and the raw body. Check it, and reject old timestamps:
// Node.js (Express: use express.raw() so you get the exact bytes)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}
# Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(want, parts["v1"])
Errors
Errors are JSON with a stable code and a message meant for a person:
{ "error": { "code": "invalid_request", "message": "`angle` is 90, 180 or 270." } }
| Status | Codes |
|---|---|
| 400 | invalid_request, invalid_file, empty_file, fetch_failed |
| 401 | missing_api_key, invalid_api_key |
| 402 | quota_exceeded: the free plan's operations for the month are used |
| 403 | link_expired: a download_url older than an hour |
| 404 | file_not_found, job_not_found, unknown_operation |
| 410 | file_expired: deleted after 24 hours |
| 413 / 415 | file_too_large / unsupported_file_type |
| 429 | rate_limited, too_many_jobs, with a Retry-After header |
| 5xx | service_unavailable, internal_error, interrupted: safe to retry |
Limits and pricing
| Free | Week Pass, Pro, Team | |
|---|---|---|
| Operations | 100 a month (UTC calendar month) | Unlimited, within fair use |
| Requests | 120 a minute per key | |
| Jobs waiting or running | 20 per account | |
| File size | 100 MB per file | |
Only operations that succeed count. Uploading, downloading, listing and polling are free. AI operations on MarqPDF's model also have their own monthly cap (20 free, 1,000 paid); on your own key they don't. GET /v1/usage returns this month's count. See pricing for the plans.
Your data
Files you send and the files operations produce are deleted after 24 hours (sooner if you DELETE them). Job records keep the operation, its options with any password removed, and file ids, for 7 days. Files are stored and processed on our servers in Google Cloud's Mumbai (India) region. All operations except extract and ask run with open-source tools there, and nothing is sent to an AI provider. extract and ask send the document to the model chosen for them: yours under your key, or MarqPDF's (Google Gemini or OpenAI). We don't train AI models on your files. Details in the privacy policy; for client documents, see the Data Processing Addendum.