Metadata-Version: 2.5
Name: pdfops
Version: 0.1.0
Summary: Python client for the PDFops API: fill AcroForm PDFs, merge PDFs, inspect form fields, generate invoices. One HTTP call, no native deps, no headless browser.
Project-URL: Homepage, https://pdfops.dev
Project-URL: Documentation, https://pdfops.dev/docs
Project-URL: Fill a PDF in Python, https://pdfops.dev/fill-pdf-in-python
Project-URL: Source, https://github.com/pdfops/pdfops-python
Project-URL: Issues, https://github.com/pdfops/pdfops-python/issues
Author-email: PDFops <hello@pdfops.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: acroform,fill-pdf,invoice,merge-pdf,pdf,pdf-api,pdf-forms,pypdf
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# pdfops

Python client for the [PDFops API](https://pdfops.dev): **fill** AcroForm PDFs, **merge** PDFs, **inspect** form fields, and **generate invoices** with one HTTP call. No native dependencies, no headless browser, nothing to install but this package.

Built for the moment `pypdf` / `PyPDFForm` stop cooperating — appearance streams that never render, `NeedAppearances` flags, encrypted government blanks, checkboxes that will not tick — and for runtimes where a PDF toolchain does not fit: Lambda, Cloud Functions, Airflow tasks, notebooks.

```bash
pip install pdfops
```

Zero dependencies (standard-library `urllib`), Python 3.9+, fully typed.

## Quick start

```python
from pdfops import PdfOps

client = PdfOps(api_key="pdfops_live_...")   # or PdfOps() for the keyless trial (100 req/IP/month)

# 1. Discover the field names — you cannot fill fields you cannot name.
info = client.inspect("blank-w9.pdf")
print(info["count"], "fields")
fields = info["fillTemplate"]                  # paste-ready {name: ""} dict

# 2. Fill, optionally flattening so the result is no longer editable.
fields["Name"] = "Ada Lovelace"
fields["Agree"] = "true"                       # checkboxes take "true" / "false"
pdf_bytes = client.fill_form("blank-w9.pdf", fields, flatten=True)
open("w9-filled.pdf", "wb").write(pdf_bytes)

# 3. Merge, in order.
packet = client.merge(["cover.pdf", pdf_bytes, "terms.pdf"])

# 4. Invoice from data — deterministic, safe to re-render from a webhook.
invoice = client.invoice({
    "from": "Acme Ltd",
    "to": {"name": "Ada Lovelace", "lines": ["12 Analytical Way", "London"]},
    "items": [{"description": "Consulting", "quantity": 3, "unit_price": 120.0}],
    "invoice_number": "2026-0042",
    "date": "2026-09-05",
    "currency": "GBP",
})
```

PDF inputs may be `bytes`, a path (`str` / `pathlib.Path`), or a binary file object. Every PDF-producing call returns `bytes`.

## API

| Method | Endpoint | Returns |
|---|---|---|
| `inspect(pdf)` | `POST /api/inspect` | `dict` — `count`, `hasXFA`, `fields[]`, `fillTemplate` |
| `fill_form(pdf, fields, *, flatten=False)` | `POST /api/fill-form` | filled PDF `bytes` |
| `merge([pdf, pdf, ...])` | `POST /api/merge` | merged PDF `bytes` |
| `invoice(request)` | `POST /api/invoice` | invoice PDF `bytes` |
| `usage()` | `GET /api/usage` | `dict` — tier, limit, used, remaining, resets_at |
| `signup(email)` | `POST /api/signup` | `None` — the key is emailed |

Full request/response schemas: [pdfops.dev/docs](https://pdfops.dev/docs) · [OpenAPI](https://pdfops.dev/openapi.json).

## Errors

Every non-2xx response raises `PdfOpsError` carrying the API's stable error slug:

```python
from pdfops import PdfOps, PdfOpsError

try:
    client.fill_form("form.pdf", {"Nmae": "typo"})
except PdfOpsError as e:
    print(e.status, e.code, e.details)   # 422 unknown_field No field named Nmae
```

Common codes: `unknown_field`, `exceeds_max_length`, `invalid_option`, `encrypted_pdf`, `rate_limited`, `invalid_api_key`.

## Configuration

- `PdfOps(api_key=...)` or the `PDFOPS_API_KEY` environment variable. A free key (250 req/month, no card) is one field at [pdfops.dev/pricing](https://pdfops.dev/pricing).
- `client_tag="..."` sets the `X-Pdfops-Client` header for anonymous usage attribution when you embed this client in another tool. Defaults to `"py"`.
- `timeout=60.0` seconds per request; `base_url=` overrides the API origin for testing.
- `transport=` injects a `(method, url, headers, body) -> (status, headers, body)` callable — the test suite uses this; no network needed.

## Why an API instead of a library

`pdf-lib` and `pypdf` are excellent when the PDF is well-behaved. PDFops exists for the other ones: it renders appearance streams server-side so every viewer shows the values, handles the encrypted-with-empty-password blanks governments publish, flattens deterministically, and is byte-for-byte reproducible — the same request yields the same file. The [compatibility registry](https://pdfops.dev/registry) records which public forms fill cleanly with which toolchain.

## Links

[Fill a PDF in Python](https://pdfops.dev/fill-pdf-in-python) · [Docs](https://pdfops.dev/docs) · [MCP server for AI agents](https://www.npmjs.com/package/pdfops-mcp) · [TypeScript SDK](https://www.npmjs.com/package/pdfops-sdk) · [n8n node](https://www.npmjs.com/package/n8n-nodes-pdfops) · hello@pdfops.dev

MIT © PDFops
