# PDF Inspector — full documentation > Hosted API and MCP server that runs Firecrawl's fast open-source pdf-inspector library (MIT) for you: any PDF to clean, LLM-ready Markdown. Pages with real text are extracted in milliseconds; scanned pages are OCR'd automatically. Zero data retention. Usable without a key; agents can create their own key. Independent service, not affiliated with Firecrawl. Website: https://pdfinspector.com/ API base URL: https://api.pdfinspector.com MCP server: https://pdfinspector.com/mcp OpenAPI: https://pdfinspector.com/openapi.json --- ## Getting access (no human needed) - **No key:** one test call per day per IP, up to 10 text pages, no OCR. There is no free tier. - **Markdown URL shortcut:** `GET https://api.pdfinspector.com/md/` returns `text/markdown`. Optional query params `pages`, `ocr`. Example: `https://api.pdfinspector.com/md/https://example.com/report.pdf` - **Plain Markdown from any endpoint:** send `Accept: text/markdown` to `/v1/convert`. - **Create a key (no auth, no signup):** ``` curl https://api.pdfinspector.com/v1/keys \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "name": "my-app", "agent": "claude-code"}' ``` ```json { "key": "sk_live_...", "credits_remaining": 0, "checkout_url": "https://pdfinspector.com/checkout/...", "dashboard_url": "https://pdfinspector.com/dashboard" } ``` - The key is returned with 0 credits. It works as soon as credits are bought through `checkout_url`. - The key is shown once. Store it as the secret `PDFINSPECTOR_API_KEY`, never in code. - Give `checkout_url` to your user. If you passed an email, they also receive it by email. Agents cannot pay. - **Check balance:** `GET /v1/usage` returns `{ plan, credits_remaining, monthly_credits, monthly_credits_remaining, monthly_resets_at, pack_credits_remaining, pack_credits_expire_at, buy_credits_url }`. - **Errors tell you what to do:** every error has `next_action` with `type` = `create_key` | `buy_credits` | `retry_after` | `fix_request`, plus `method`/`url` or `retry_after_seconds`. For `buy_credits`, give the URL to your user (payment is always done by a human). --- ## Rules for AI agents writing an integration 1. Read the API key from an environment variable or secret named `PDFINSPECTOR_API_KEY`. 2. If the key starts with `sk_`, only use it in server code (API route, edge function, backend). Never put it in frontend code or commit it. 3. If the app has no server at all, use a publishable `pk_` key from `VITE_PDFINSPECTOR_PK` / `NEXT_PUBLIC_PDFINSPECTOR_PK`; the user must add their domain to the key's allowed origins in the dashboard. 4. Send PDFs as `multipart/form-data` with the field name `file`. 5. Show a progress state: documents with scanned pages take a few seconds. 6. On a non-2xx response, show `error.message` to the user. It is written to be shown as-is. 7. If the response status is 202, poll `GET /v1/jobs/{id}` every 3 seconds until `status` is `completed` or `failed`. 8. The main output is `markdown`. Render it with a Markdown renderer that supports tables (GFM). 9. PDF Inspector does not extract fields. If the user wants fields (invoice number, total, dates, names), send `markdown` to the app's LLM and ask for JSON. --- ## What it does and doesn't do Does: - Convert PDFs to Markdown: headings (H1–H4), lists, bold/italic, links, code blocks, tables (ruled and borderless, incl. financial tables and tables continuing across pages), multi-column reading order. Page numbers and hyphenation are cleaned up. - OCR scanned or image-only pages automatically (`ocr: "auto"`), including table structure. Pages with real text never go through OCR. - Classify PDFs: `text_based`, `scanned`, `image_based`, `mixed`. - Return document metadata (title, author, subject, keywords, creator, producer, dates). - Return positioned text items (coordinates, font) for pages with real text. Doesn't: - Extract named fields or answer questions (use an LLM on the Markdown). - Describe images or extract chart data. - Store files or results (zero data retention). --- ## Authentication Header: `Authorization: Bearer ` - `sk_live_...` secret key, server-side only. - `pk_live_...` publishable key, allowed in browsers, restricted to allowed origins and a monthly page cap. - `sk_test_...` test key, returns sample output, uses no credits. --- ## POST /v1/convert Content types: `multipart/form-data` or `application/json`. | name | type | description | |---|---|---| | file | file | The PDF (multipart). Max 50 MB. | | url | string | Public URL of a PDF. | | file_base64 | string | Base64 PDF (JSON body). | | pages | string | 1-indexed page selection, e.g. "1-3,7". Only these pages are processed and charged. | | ocr | string | "auto" (default): OCR only pages without real text. "off": never OCR; scanned pages listed in pages_needing_ocr. "force": OCR every selected page. | | per_page | boolean | Also return `pages` array with per-page markdown and source. Default false. | | webhook_url | string | Run as a job and POST the result here. | Exactly one of `file`, `url`, `file_base64` is required. ### Example: curl ``` curl https://api.pdfinspector.com/v1/convert \ -H "Authorization: Bearer $PDFINSPECTOR_API_KEY" \ -F file=@report.pdf ``` ### Example: JavaScript (server) ```js const form = new FormData(); form.append("file", pdfFile); const res = await fetch("https://api.pdfinspector.com/v1/convert", { method: "POST", headers: { Authorization: `Bearer ${process.env.PDFINSPECTOR_API_KEY}` }, body: form, }); const data = await res.json(); if (!res.ok) throw new Error(data.error.message); console.log(data.markdown); ``` ### Example: Supabase / Lovable edge function (Deno) ```ts // supabase/functions/pdf-to-markdown/index.ts Deno.serve(async (req) => { const form = await req.formData(); // contains "file" const res = await fetch("https://api.pdfinspector.com/v1/convert", { method: "POST", headers: { Authorization: `Bearer ${Deno.env.get("PDFINSPECTOR_API_KEY")}` }, body: form, }); return new Response(await res.text(), { status: res.status, headers: { "Content-Type": "application/json" }, }); }); ``` ### Example: Next.js route handler (v0) ```ts // app/api/pdf/route.ts export async function POST(req: Request) { const form = await req.formData(); const res = await fetch("https://api.pdfinspector.com/v1/convert", { method: "POST", headers: { Authorization: `Bearer ${process.env.PDFINSPECTOR_API_KEY}` }, body: form, }); return new Response(await res.text(), { status: res.status, headers: { "Content-Type": "application/json" } }); } ``` ### Example: Python ```python import os, requests res = requests.post( "https://api.pdfinspector.com/v1/convert", headers={"Authorization": f"Bearer {os.environ['PDFINSPECTOR_API_KEY']}"}, files={"file": open("report.pdf", "rb")}, ) res.raise_for_status() print(res.json()["markdown"]) ``` ### Example: getting fields with your own LLM ``` System: You extract data from documents. Reply with JSON only. User: From this invoice, return {"invoice_number": string, "date": "YYYY-MM-DD", "total": number, "vendor": string}. {markdown from PDF Inspector} ``` --- ## Response ```json { "pdf_type": "mixed", "confidence": 0.97, "page_count": 12, "markdown": "# Q3 Report\n\n## Revenue\n\n| Region | Q2 | Q3 |\n|---|---|---|\n| EU | 1.2M | 1.4M |", "pages_ocr": [11, 12], "pages_needing_ocr": [], "pages_with_tables": [2, 3, 7], "pages_with_columns": [4], "has_encoding_issues": false, "metadata": { "title": "Q3 Report", "author": "Finance Team", "subject": null, "keywords": null, "creator": "Microsoft Word", "producer": "macOS Quartz", "creation_date": "D:20260901103000+02'00'", "mod_date": null }, "pages": [ { "page": 1, "markdown": "# Q3 Report\n...", "source": "native" }, { "page": 11, "markdown": "...", "source": "ocr" } ], "processing_time_ms": 3140, "usage": { "pages": 10, "ocr_pages": 2, "credits_used": 40, "credits_remaining": 19960 } } ``` - `pdf_type`: "text_based" | "scanned" | "image_based" | "mixed" - `pages_ocr`: pages whose text came from OCR. - `pages_needing_ocr`: scanned pages that were NOT OCR'd (only non-empty with `ocr: "off"`). - `pages`: only present with `per_page: true`. `source` is "native" or "ocr". - `usage.pages` = regular pages (1 credit each), `usage.ocr_pages` = OCR pages (15 credits each). A page is never counted twice. - `notices`: human/agent-readable hints (e.g. scanned pages skipped on anonymous calls). - Metadata dates are returned as written in the PDF. ## OCR behavior With `ocr: "auto"` (default), every page is checked first. Pages with real text are extracted directly (milliseconds, regular page). Pages that are scanned, image-only or have broken font encodings are OCR'd, including table structure (seconds, OCR page). Results are merged in page order. ## POST /v1/inspect Classify without converting. Same inputs as /v1/convert. Uses no credits. Usually well under a second. ```json { "pdf_type": "scanned", "confidence": 0.99, "page_count": 48, "pages_needing_ocr": [1, 2, 3], "pages_with_tables": [], "pages_with_columns": [], "is_complex_layout": false, "has_encoding_issues": false, "metadata": { "title": null, "author": null, "producer": "Canon iR-ADV C5535" } } ``` ## POST /v1/text-items (advanced) Positioned text runs for pages with real text (no OCR). Accepts `pages`. ```json { "items": [ { "text": "Revenue", "page": 2, "x": 72.0, "y": 690.4, "width": 58.2, "height": 14.0, "rotation": 0, "font": "Helvetica-Bold", "font_size": 14, "is_bold": true, "is_italic": false } ] } ``` Coordinates: PDF points from the lower-left corner of the visible page box, y up. ## Long documents: GET /v1/jobs/{id} - Up to 50 pages: same-request 200. - More than 50 pages, or `webhook_url` set: 202 `{ "id": "job_...", "status": "processing" }`. - Poll `GET /v1/jobs/{id}` until `status` is "completed" (result included) or "failed" (error included). - Webhook POST carries header `PDFInspector-Signature`: HMAC-SHA256 of the raw body using your webhook secret. - Job results are deleted after the first successful fetch or after 1 hour, whichever comes first. ## Zero data retention - PDFs and results exist only while the request is being processed and are deleted automatically right after. No permanent storage, no archive. - File contents and extracted text are never written to our logs or error reports. - Nothing is used for training. - Only billing metadata is kept: timestamp, key id, page counts, processing time. ## Errors Shape: `{ "error": { "code": "...", "message": "...", "next_action": { "type": "...", "method": "...", "url": "..." }, "docs": "..." } }` | status | code | meaning | |---|---|---| | 400 | invalid_request | Missing file/url or bad parameter | | 401 | invalid_key | Key missing, wrong or revoked | | 402 | insufficient_credits | Not enough credits, or key spend cap reached (next_action: buy_credits) | | 429 | anonymous_limit | Daily test call without a key used (next_action: create_key) | | 403 | origin_not_allowed | Publishable key used from an unlisted domain | | 413 | file_too_large | Over 50 MB or 500 pages | | 422 | unreadable_pdf | Corrupt, not a PDF, or password-protected | | 429 | rate_limited | Retry after the Retry-After header | ## Limits - 50 MB and 500 pages per file. - No key: one test call per day per IP, up to 10 text pages, no OCR. - Rate limits: credit packs 30/min, Builder 60/min, Scale higher. - Failed requests and /v1/inspect use no credits. --- ## MCP server - URL: https://pdfinspector.com/mcp - Transport: Streamable HTTP - Auth: none for the daily test call; otherwise OAuth 2.1 with dynamic client registration, or `Authorization: Bearer sk_...` Tools: - `pdf_to_markdown`: inputs `url` or `file_base64`; optional `pages`, `ocr`. Returns the same JSON as POST /v1/convert. - `inspect_pdf`: inputs `url` or `file_base64`. Returns the same JSON as POST /v1/inspect. - `get_job`: input `id`. Returns status and result of a long-document job. - `create_api_key`: optional `email`, `name`. Returns a new key with 0 credits and a checkout link (same as POST /v1/keys). - `get_usage`: no input. Returns plan and remaining pages. ### Client setup Claude Code: ``` claude mcp add --transport http pdfinspector https://pdfinspector.com/mcp ``` Cursor (`.cursor/mcp.json`): ```json { "mcpServers": { "pdfinspector": { "url": "https://pdfinspector.com/mcp", "headers": { "Authorization": "Bearer sk_live_YOUR_KEY" } } } } ``` Windsurf (`~/.codeium/windsurf/mcp_config.json`): ```json { "mcpServers": { "pdfinspector": { "serverUrl": "https://pdfinspector.com/mcp", "headers": { "Authorization": "Bearer sk_live_YOUR_KEY" } } } } ``` Claude (web/desktop): Settings → Connectors → Add custom connector → URL `https://pdfinspector.com/mcp` → Connect and sign in. --- ## Integration prompts Lovable: ``` Add PDF upload using PDF Inspector. Read https://pdfinspector.com/llms.txt first. Create an edge function "pdf-to-markdown" that forwards the uploaded PDF as multipart/form-data (field "file") to POST https://api.pdfinspector.com/v1/convert with "Authorization: Bearer ${PDFINSPECTOR_API_KEY}". Store PDFINSPECTOR_API_KEY as a secret. Show a progress state, render the returned markdown, save it to a "documents" table, and show error.message on failure. ``` Bolt / Replit / any builder: ``` Integrate PDF Inspector to turn PDFs into Markdown. Read https://pdfinspector.com/llms.txt and follow it exactly. Keep the API key server-side. ``` --- ## Pricing - There is no free tier. Without a key: one test call per day, up to 10 text pages, no OCR. - Credits: 1 page with real text = 1 credit. 1 page that needs OCR = 15 credits. /v1/inspect and failed requests use no credits. - Credit packs: $10 = 10,000 credits ($0.001/page), $50 = 60,000 credits. Valid 12 months. Optional auto top-up. - Builder: $12/month = 20,000 credits/month, 60 requests/min. - Scale: $49/month = 100,000 credits/month, priority queue, invoice billing. - Subscription credits reset monthly and do not roll over. They are used before pack credits. - Spend caps can be set per key.