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.

  1. Create a free account, confirm your email, then open Account → Developers / API and create a key.
  2. 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..."}
  1. Poll until status is completed, 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 accountPremium
API jobs per calendar month (UTC)1002,000
Requests per minute, per key6060
Max size per input file50 MB50 MB
Max pages per job30200
AI tools (OCR, summarize, AI Word engine)NoYes, within the fair-use allowance
PDF outputSmall "PDFBEAR.COM - Free Ver." badge in the lower-right corner of each pageClean, no badge
CORS (browser calls)Allowed (Access-Control-Allow-Origin: *)Allowed
Result link lifetime30 daysWhile your subscription is active
About the free badge. API output follows exactly the same rule as the website: on a free account, every PDF result gets a small "PDFBEAR.COM - Free Ver." stamp in the lower-right corner of each page. ZIP, DOCX, JPG and TXT results are not stamped, and neither are encrypted PDFs from 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:

FieldDescription
toolRequired. A tool_id from the table below, e.g. compress-pdf.
file / filesMultipart upload. merge-pdf takes 2 or more files in order.
file_url / file_urlsA public http(s) URL we download the input from (JSON or form). Private and internal addresses are refused.
content_base64 + filenameJSON only: the file inline, base64-encoded. files: [{filename, content_base64}] for merges.
urlhtml-to-pdf: the web page to render. For other tools it is treated like file_url.
optionsTool 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_idWhat it doesInputOutputOptionsPlan
compress-pdf Reduce PDF file size (lossless or with image downsampling). .pdf PDF compression_level (less | recommended | extreme) Free
merge-pdf Combine 2 or more PDFs into one, in the order given. 2+ .pdf PDF - Free
split-pdf Split a PDF into several PDFs (returned as a ZIP). .pdf 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). .pdf DOCX engine (standard | ai) Free
word-to-pdf Convert a Word document (DOC/DOCX/ODT/RTF) to PDF. .doc, .docx, .odt, .rtf PDF - Free
pdf-to-jpg Render every PDF page as a JPG image (returned as a ZIP). .pdf ZIP - Free
jpg-to-pdf Convert a JPG or PNG image to a PDF. .jpg, .jpeg, .png PDF - Free
rotate-pdf Rotate all pages, or selected pages, of a PDF. .pdf PDF angle (90 | 180 | 270)
pages (string)
Free
protect-pdf Encrypt a PDF with an open password. .pdf PDF password (string), required Free
unlock-pdf Remove the password/encryption from a PDF you are allowed to open. .pdf PDF password (string)
guess_password (boolean)
Free
remove-watermark-pdf Remove text/image/annotation watermarks from a PDF (automatic structural mode). .pdf PDF watermark_text (string)
target_color (string)
Free
pdf-to-pdfa Convert a PDF to PDF/A-2b for long-term archiving. .pdf PDF - Free
html-to-pdf Render a public web page (http/https URL) to PDF, JavaScript included. web page URL PDF 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). .pdf PDF - Premium
summarize-pdf AI summary of a PDF, returned as text (Premium). .pdf 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.

HTTPcodeMeaning
400bad_requestThe body is not multipart or a JSON object.
401missing_api_key, invalid_api_key, api_key_revokedSend a valid key as Authorization: Bearer ....
403email_unverifiedConfirm the email address on your account.
403premium_requiredThe tool or option needs Premium (AI tools, scanned PDFs, password recovery).
404job_not_found, not_foundNo such job on your account, or no such endpoint.
409job_not_ready, job_failedDownload requested before the job completed, or after it failed.
410result_expiredThe result is past its expiry date. Run the job again.
413file_too_large, too_many_pages, too_many_filesThe input is over a size, page or file-count limit.
422validation_error, unknown_option, missing_input, unsupported_tool, unsupported_file_type, empty_file, unsafe_url, url_fetch_failedThe request is well-formed but can't be processed as sent.
429rate_limitedOver 60 requests a minute on this key. Wait for Retry-After seconds.
429quota_exceededThis month's job quota is used up. Retry-After points at the reset.
429ai_allowance_exhaustedThe Premium fair-use AI allowance for this billing period is used up.
500internal_errorOur 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

Questions or a tool you'd like to see in the API? Email support@pdfbear.com.