PDFBEAR API and MCP server for developers
Compress, convert, merge, split, protect, unlock and OCR PDFs from your own code with a simple REST API, or let an AI assistant do it through our remote MCP server. The same engine that runs pdfbear.com, with a free tier of 100 jobs a month.
Quick start
Every conversion is an asynchronous job: you start it, poll its status, then download the result. Base URL: https://pdfbear.com/api/v1.
- Create a free account, confirm your email, then open Account → Developers / API and create a key.
- Start a job:
curl -X POST https://pdfbear.com/api/v1/jobs \
-H "Authorization: Bearer $PDFBEAR_API_KEY" \
-F tool=compress-pdf \
-F file=@report.pdf \
-F 'options={"compression_level":"recommended"}'
# 202 Accepted
{"job_id": "job_3f2c9a0e8b7d4c1e9f6a5b4c3d2e1f00", "status": "queued",
"tool": "compress-pdf", "status_url": "https://pdfbear.com/api/v1/jobs/job_3f2c..."}
- Poll until
statusiscompleted, then download:
curl https://pdfbear.com/api/v1/jobs/job_3f2c9a0e8b7d4c1e9f6a5b4c3d2e1f00 \
-H "Authorization: Bearer $PDFBEAR_API_KEY"
curl -L -o report-small.pdf \
https://pdfbear.com/api/v1/jobs/job_3f2c9a0e8b7d4c1e9f6a5b4c3d2e1f00/download \
-H "Authorization: Bearer $PDFBEAR_API_KEY"
Most jobs finish in a few seconds. Poll every 1-2 seconds; status requests count toward the rate limit but not toward your monthly job quota.
Authentication
Send your key in the Authorization header on every request: Authorization: Bearer pdfb_live_.... Keys are 42 characters long and start with pdfb_live_. We store only a SHA-256 fingerprint, so a key is shown once when you create it; if you lose it, revoke it and create a new one. Keys never expire, and revoking one takes effect immediately.
Keep keys on your server. Anyone who has a key can run jobs on your quota.
CORS: /api/v1 and /mcp send Access-Control-Allow-Origin: *, so browsers are allowed to call them (no cookies are used; the key is the only credential). Only do that from a private tool or an app where the user enters their own key: a key embedded in a public web page can be copied by anyone. For public sites, call the API from your server.
Plans, quotas and limits
Free tier at a glance: 100 jobs per month, 60 requests per minute per key, 50 MB per file, no credit card.
| Free account | Premium | |
|---|---|---|
| API jobs per calendar month (UTC) | 100 | 2,000 |
| Requests per minute, per key | 60 | 60 |
| Max size per input file | 50 MB | 50 MB |
| Max pages per job | 30 | 200 |
| AI tools (OCR, summarize, AI Word engine) | No | Yes, within the fair-use allowance |
| PDF output | Small "PDFBEAR.COM - Free Ver." badge in the lower-right corner of each page | Clean, no badge |
| CORS (browser calls) | Allowed (Access-Control-Allow-Origin: *) | Allowed |
| Result link lifetime | 30 days | While your subscription is active |
protect-pdf. Premium output is always clean. Each job's status says whether its result carries the badge (free_badge).Every created job counts toward the monthly quota, including jobs that later fail. Requests that are rejected before a job is created (bad key, invalid options, a file that is too large) do not count. GET /api/v1/me shows what you have left. Total upload size per request is capped at 50 MB; for bigger merges, pass file_urls instead.
Endpoints
GET/api/v1/tools
Lists every tool the API offers with its accepted input types, output format, option schema and whether it needs Premium. No key needed.
POST/api/v1/jobs
Starts a job and returns 202 Accepted with job_id and status_url. Send either multipart/form-data or JSON:
| Field | Description |
|---|---|
tool | Required. A tool_id from the table below, e.g. compress-pdf. |
file / files | Multipart upload. merge-pdf takes 2 or more files in order. |
file_url / file_urls | A public http(s) URL we download the input from (JSON or form). Private and internal addresses are refused. |
content_base64 + filename | JSON only: the file inline, base64-encoded. files: [{filename, content_base64}] for merges. |
url | html-to-pdf: the web page to render. For other tools it is treated like file_url. |
options | Tool options: a JSON object (in multipart, a JSON string). Unknown options are rejected. |
curl -X POST https://pdfbear.com/api/v1/jobs \
-H "Authorization: Bearer $PDFBEAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tool": "html-to-pdf", "url": "https://example.com",
"options": {"page_size": "A4", "remove_popups": true}}'
curl -X POST https://pdfbear.com/api/v1/jobs \
-H "Authorization: Bearer $PDFBEAR_API_KEY" \
-F tool=merge-pdf -F files=@part1.pdf -F files=@part2.pdf
GET/api/v1/jobs/{job_id}
Returns status: queued, processing, completed or failed. A failed job includes error.message. A completed job includes:
{
"job_id": "job_3f2c9a0e8b7d4c1e9f6a5b4c3d2e1f00",
"tool": "compress-pdf",
"status": "completed",
"created_at": "2026-09-26T10:15:02Z",
"finished_at": "2026-09-26T10:15:05Z",
"input": {"filename": "report.pdf", "size": 4812331},
"download_url": "https://pdfbear.com/api/v1/jobs/job_3f2c.../download",
"public_url": "https://pdfbear.com/downloads/converted/converted_9b1e....pdf",
"filename": "report.pdf",
"size": 1022456,
"content_type": "application/pdf",
"expires_at": "2026-10-26T10:15:02Z",
"expired": false,
"free_badge": true
}
download_url needs your key. public_url is the same unguessable, expiring link the website gives you, and it opens in any browser without a key, so treat it like a password-free share link.
GET/api/v1/jobs/{job_id}/download
Streams the result file with a proper Content-Disposition filename. Returns 409 while the job is still running and 410 once the result has expired.
GET/api/v1/me
Your plan, this month's job quota (limit, used, remaining, resets_at) and your limits.
Tools and options
| tool_id | What it does | Input | Output | Options | Plan |
|---|---|---|---|---|---|
compress-pdf |
Reduce PDF file size (lossless or with image downsampling). | compression_level (less | recommended | extreme) |
Free | ||
merge-pdf |
Combine 2 or more PDFs into one, in the order given. | 2+ .pdf | - | Free | |
split-pdf |
Split a PDF into several PDFs (returned as a ZIP). | ZIP | mode (all | ranges | fixed | size)ranges (string)merge_ranges (boolean)every_n_pages (integer)max_size_mb (number) |
Free | |
pdf-to-word |
Convert a PDF to an editable Word document (.docx). | DOCX | engine (standard | ai) |
Free | |
word-to-pdf |
Convert a Word document (DOC/DOCX/ODT/RTF) to PDF. | .doc, .docx, .odt, .rtf | - | Free | |
pdf-to-jpg |
Render every PDF page as a JPG image (returned as a ZIP). | ZIP | - | Free | |
jpg-to-pdf |
Convert a JPG or PNG image to a PDF. | .jpg, .jpeg, .png | - | Free | |
rotate-pdf |
Rotate all pages, or selected pages, of a PDF. | angle (90 | 180 | 270)pages (string) |
Free | ||
protect-pdf |
Encrypt a PDF with an open password. | password (string), required |
Free | ||
unlock-pdf |
Remove the password/encryption from a PDF you are allowed to open. | password (string)guess_password (boolean) |
Free | ||
remove-watermark-pdf |
Remove text/image/annotation watermarks from a PDF (automatic structural mode). | watermark_text (string)target_color (string) |
Free | ||
pdf-to-pdfa |
Convert a PDF to PDF/A-2b for long-term archiving. | - | Free | ||
html-to-pdf |
Render a public web page (http/https URL) to PDF, JavaScript included. | web page URL | page_size (A3 | A4 | A5 | Letter)orientation (portrait | landscape)margin (none | small | big)screen_width (integer)one_long_page (boolean)block_ads (boolean)remove_popups (boolean) |
Free | |
ocr-pdf |
Make a scanned PDF searchable with AI OCR (Premium). | - | Premium | ||
summarize-pdf |
AI summary of a PDF, returned as text (Premium). | TXT | length (short | medium | long)tone (neutral | formal | casual | academic | simple) |
Premium |
Notes: pdf-to-word with a scanned (image-only) PDF needs AI OCR, which is Premium. unlock-pdf is for files you are allowed to open; guess_password (trying common passwords) is Premium. remove-watermark-pdf runs the automatic mode; on a free account the output still carries the PDFBEAR free badge. The full machine-readable list is at /api/v1/tools.
Python example
import json, os, time, requests
API = "https://pdfbear.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PDFBEAR_API_KEY']}"}
def run(tool, path, **options):
with open(path, "rb") as fh:
r = requests.post(f"{API}/jobs", headers=HEADERS,
data={"tool": tool, "options": json.dumps(options)},
files={"file": fh})
r.raise_for_status()
job = r.json()
while job["status"] in ("queued", "processing"):
time.sleep(1.5)
job = requests.get(job["status_url"], headers=HEADERS).json()
if job["status"] == "failed":
raise RuntimeError(job["error"]["message"])
out = requests.get(job["download_url"], headers=HEADERS)
out.raise_for_status()
with open(job["filename"], "wb") as fh:
fh.write(out.content)
return job["filename"]
print(run("compress-pdf", "report.pdf", compression_level="extreme"))
JavaScript example
Node.js 20 or newer (built-in fetch, FormData and fs.openAsBlob):
import { openAsBlob } from "node:fs";
import { writeFile } from "node:fs/promises";
const API = "https://pdfbear.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.PDFBEAR_API_KEY}` };
async function run(tool, path, options = {}) {
const form = new FormData();
form.append("tool", tool);
form.append("options", JSON.stringify(options));
form.append("file", await openAsBlob(path), path.split("/").pop());
let res = await fetch(`${API}/jobs`, { method: "POST", headers, body: form });
let job = await res.json();
if (!res.ok) throw new Error(job.error.message);
while (job.status === "queued" || job.status === "processing") {
await new Promise((r) => setTimeout(r, 1500));
job = await (await fetch(job.status_url, { headers })).json();
}
if (job.status === "failed") throw new Error(job.error.message);
const file = await fetch(job.download_url, { headers });
await writeFile(job.filename, Buffer.from(await file.arrayBuffer()));
return job.filename;
}
console.log(await run("pdf-to-word", "contract.pdf"));
Errors
Errors always look like {"error": {"code": "quota_exceeded", "message": "..."}}. Branch on code; the message is written for humans.
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_request | The body is not multipart or a JSON object. |
| 401 | missing_api_key, invalid_api_key, api_key_revoked | Send a valid key as Authorization: Bearer .... |
| 403 | email_unverified | Confirm the email address on your account. |
| 403 | premium_required | The tool or option needs Premium (AI tools, scanned PDFs, password recovery). |
| 404 | job_not_found, not_found | No such job on your account, or no such endpoint. |
| 409 | job_not_ready, job_failed | Download requested before the job completed, or after it failed. |
| 410 | result_expired | The result is past its expiry date. Run the job again. |
| 413 | file_too_large, too_many_pages, too_many_files | The input is over a size, page or file-count limit. |
| 422 | validation_error, unknown_option, missing_input, unsupported_tool, unsupported_file_type, empty_file, unsafe_url, url_fetch_failed | The request is well-formed but can't be processed as sent. |
| 429 | rate_limited | Over 60 requests a minute on this key. Wait for Retry-After seconds. |
| 429 | quota_exceeded | This month's job quota is used up. Retry-After points at the reset. |
| 429 | ai_allowance_exhausted | The Premium fair-use AI allowance for this billing period is used up. |
| 500 | internal_error | Our fault. Retry, and contact support if it keeps happening. |
A job that is accepted but then fails (for example a corrupted PDF) is not an HTTP error: the job's status is failed and error.message explains what went wrong.
MCP server
PDFBEAR runs a remote Model Context Protocol server, so AI assistants such as Claude, Cursor and VS Code agents can use PDFBEAR tools directly. Endpoint: https://pdfbear.com/mcp (Streamable HTTP transport, stateless). It uses the same API key, quota and plan rules as the REST API.
Each API tool is an MCP tool (compress_pdf, merge_pdf, pdf_to_word, html_to_pdf, ...) plus get_job. Give a tool a file_url it can download, or a small file (up to 10 MB) as content_base64 + filename. The call waits for the job and returns a download link; if the job needs longer, it returns the job_id to check with get_job.
Claude Code
claude mcp add --transport http pdfbear https://pdfbear.com/mcp \
--header "Authorization: Bearer pdfb_live_..."
Claude Desktop
Add this to claude_desktop_config.json (Settings → Developer → Edit Config). It uses the mcp-remote bridge, which needs Node.js:
{
"mcpServers": {
"pdfbear": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://pdfbear.com/mcp",
"--header", "Authorization:${PDFBEAR_AUTH}"],
"env": { "PDFBEAR_AUTH": "Bearer pdfb_live_..." }
}
}
}
Cursor, VS Code and other MCP clients
Any client that supports remote HTTP servers with custom headers works. Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"pdfbear": {
"url": "https://pdfbear.com/mcp",
"headers": { "Authorization": "Bearer pdfb_live_..." }
}
}
}
VS Code (.vscode/mcp.json):
{
"servers": {
"pdfbear": {
"type": "http",
"url": "https://pdfbear.com/mcp",
"headers": { "Authorization": "Bearer pdfb_live_..." }
}
}
}
For a raw client: POST JSON-RPC 2.0 messages to /mcp with Content-Type: application/json. The server supports protocol versions 2025-06-18, 2025-03-26 and 2024-11-05, answers every request with a single JSON response, and returns 405 for GET (there is no server-sent event stream). initialize, ping and tools/list work without a key so you can inspect the tools; tools/call needs one.
curl https://pdfbear.com/mcp -H "Content-Type: application/json" \
-H "Authorization: Bearer $PDFBEAR_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"compress_pdf",
"arguments":{"file_url":"https://example.com/report.pdf","compression_level":"extreme"}}}'
Changelog
- v1.0.0 (2026-09-26): first public release. REST API with 15 tools, API keys, monthly quotas, OpenAPI 3.1 spec, and the remote MCP server.
Questions or a tool you'd like to see in the API? Email support@pdfbear.com.