# 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.

[Get an API key](https://marqpdf.com/?dev=keys) · [OpenAPI spec](https://marqpdf.com/v1/openapi.json)

- **18 operations, workflows, an agent**: Compress to a size, OCR in English and Arabic, convert, merge, split, protect; AI extraction and Q&A on your own model; saved chains of steps; an agent that plans them for you.

- **SDKs and AI agents**: JavaScript and Python SDKs, and an MCP server so Claude, Cursor and other agents can work on PDFs.

- **100 free a month**: Free accounts get 100 operations a month. A Week Pass, Pro or Team plan is unlimited.

## Quick start

Create a key under [Account → Developers](https://marqpdf.com/?dev=keys), 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](https://marqpdf.com/v1/openapi.json).

## AI agents (MCP)

MarqPDF is an [MCP](https://modelcontextprotocol.io) 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](https://marqpdf.com/?dev=keys) 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` optional; `level` optional (screen \| ebook \| printer \| prepress) | Compress a PDF. Smallest file that fits under `target_mb`, or a fixed quality `level`. [In the browser](https://marqpdf.com/compress-pdf) |
| `POST /v1/merge` | `files` | — | Merge PDFs. Join 2–50 PDFs, in the order given. [In the browser](https://marqpdf.com/merge-pdf) |
| `POST /v1/split` | `file` | `every` optional; `ranges` optional | Split a PDF. A ZIP of PDFs: every N pages, or the page ranges you give. [In the browser](https://marqpdf.com/split-pdf) |
| `POST /v1/rotate` | `file` | `angle` (90 \| 180 \| 270); `pages` optional | Rotate pages. Counts as one operation when it succeeds. [In the browser](https://marqpdf.com/rotate-pdf) |
| `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](https://marqpdf.com/watermark-pdf) |
| `POST /v1/protect` | `file` | `password` | Password-protect a PDF. Counts as one operation when it succeeds. [In the browser](https://marqpdf.com/protect-pdf) |
| `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](https://marqpdf.com/unlock-pdf) |
| `POST /v1/ocr` | `file` | — | Make a scanned PDF searchable. Adds a text layer (English and Arabic) under the page images. [In the browser](https://marqpdf.com/ocr-pdf) |
| `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](https://marqpdf.com/scanned-pdf-to-word) |
| `POST /v1/images-to-pdf` | `files` | — | Images to one PDF. One page per image, in the order given. [In the browser](https://marqpdf.com/jpg-to-pdf) |
| `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](https://marqpdf.com/extract-text-from-pdf) |
| `POST /v1/extract` | `file` | `fields` optional; `schema` optional; `instructions` optional; `model` 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` | `question`; `instructions` optional; `model` 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` | `goal`; `max_steps` optional; `model` 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](https://marqpdf.com/developers#jobs)) and `webhook_url` (see [webhooks](https://marqpdf.com/developers#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=,v1=` 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](https://marqpdf.com/?page=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](https://marqpdf.com/?page=privacy); for client documents, see the [Data Processing Addendum](https://marqpdf.com/?page=dpa).

> Questions, or an operation you need that isn't here? Email [support@marqpdf.com](https://marqpdf.commailto:support@marqpdf.com).
