Pinned
Every PDF API assumes a server with a headless browser. You can't run that in a V8 isolate. PDFops is the fill + merge primitive built for the edge: one HTTP call, deterministic bytes out, no native deps. Free tier, no signup.
PDFops
@pdfops.dev
Hosted PDF API for AI agents, n8n and Python. Fill AcroForm fields, merge PDFs, inspect forms, generate invoices. Deterministic output, no headless browser. MCP server + SDKs, free key at pdfops.dev
87 followers324 following912 posts
Pinned
Every PDF API assumes a server with a headless browser. You can't run that in a V8 isolate. PDFops is the fill + merge primitive built for the edge: one HTTP call, deterministic bytes out, no native deps. Free tier, no signup.
Every IRS W-9 we've seen in the wild has the same field names across versions, but almost nobody has them memorized. We keep a registry of pre-mapped common forms so you can fill one without opening it in a viewer first. →
pdf-lib is solid for building PDFs from scratch in JS — it's a different job from filling an existing form's AcroForm fields over HTTP. Breakdown of where each one actually fits →
An AI agent that can fill a PDF needs exactly one tool call, not a browser. We shipped an MCP server so agents can inspect a form's fields and fill it without anyone writing glue code. How it's wired →
Deno Deploy has no filesystem and no native deps — which kills most PDF libs before you even get to the KYC packet you're trying to assemble. Here's a KYC flow (ID doc + consent form + signature page) running entirely on it. #serverless
wkhtmltopdf has been unmaintained for years and still ships in a lot of Lambda layers. We migrated a production invoice pipeline off it — no headless renderer, no layer size limit, same output shape. What changed →
Snapshot-testing PDF output in CI usually fails for dumb reasons: a timestamp byte or a random object ID changes and your diff lights up red on every run. Zero those two things and PDF snapshot tests behave like any other fixture. #webdev
An invoice line-item table that overflows one page used to mean hand-rolled pagination logic. We built the overflow handling into the template once and now every invoice just... fits. Walkthrough →
Merging 40 PDFs where half are Letter and half are A4? Naive merges either stretch pages or crop content. We wrote up how to preserve each page's native size in one merged output, no re-render. #webdev
Filling a government PDF form and getting garbage back? Check if it's encrypted first — some IRS and USCIS forms ship with owner-password restrictions that silently break naive AcroForm writers. We hit this with a real I-9. Fix and code →
New on the blog: A fixed-row invoice template, and the line item that doesn't fit. An AcroForm invoice template has a fixed number of line-item rows. When an order has more items than that, the fix isn't a bigger template, it's a continuation page filled and merged in. →
A contract PDF usually has 10-30 fields and needs to look identical every time it's signed — which rules out any pipeline where layout can drift between runs. Deterministic fill is the actual requirement here, not a nice-to-have. Use case →
An invoice is the simplest PDF use case there is: fixed layout, same fields every time, numbers change. That simplicity is exactly why people reach for a rendering engine when a fill call would do. Use case breakdown with the actual request/response →
DocRaptor renders HTML/CSS to PDF through a hosted Prince XML engine — great for generating a new document from markup, overkill for stamping values into a form that already exists as a PDF. Comparison for the form-fill case specifically →
Anvil's PDF filling runs through their own document builder and pricing tiers by seat. If you already have a PDF template and just need fields set, that's a flatter problem than Anvil is built for. Where the two actually diverge →
n8n doesn't have a native PDF-fill node, so most workflows fake it with an HTML-to-PDF node that ignores your actual template. One HTTP Request node pointed at a fill API handles AcroForm fields n8n otherwise can't touch. #webdev
Tally form submits, webhook fires, a signed contract PDF needs to exist on the other end — all inside a Worker's request lifetime, no server to poll. The field-mapping trick that makes a generic Tally form reusable across contract templates. #webdev
A warehouse webhook fires on order-packed, and the packing slip has to exist before the label printer polls for it — so the fill call sits in the request path, not a queue. What breaks when you treat PDF generation as fire-and-forget here. #webdev
A monthly bundle job — same five report templates, new numbers, zipped and emailed on a cron — doesn't need a task queue. It needs five fill calls and a merge, run from whatever already triggers your cron. Walkthrough →
pypdf's merge throws or silently reflows pages when inputs mix Letter and A4 — because merging page trees isn't the same as merging page sizes. What actually happens to the media box, and the one setting that keeps mixed-size merges sane. #webdev
An AI agent that fills PDF forms doesn't need a browser tool or a code-exec sandbox — it needs one MCP tool that takes a field map and returns bytes. We wrote up the tool schema and why it keeps the agent's action space small. #webdev
Stripe webhook fires, you need a PDF invoice back in the response before the customer's browser times out. No queue, no background job — fill the template inline inside the Worker and return bytes. How it holds up under Stripe's retry behavior →
KYC packet with ID scan, address proof, and a signed consent form, assembled on Deno Deploy where there's no filesystem to shell out to pdftk. One POST per document, merged in field order, deterministic bytes out. Notes from building it →
New on the blog: The 4.5 MB request limit that breaks a working PDF merge on Vercel. A PDF merge endpoint that works on a laptop can start returning an empty 413 the moment it moves to Vercel. The platform-level request-body cap that causes it, why local testing never catches it, →
New on the blog: Flattening a filled PDF form with pypdf, and the method that doesn't exist. pypdf fills AcroForm fields but ships no public flatten() method, and its only private _flatten() does something else entirely: collapse page-tree inheritance, not bake form values into t →
wkhtmltopdf is unmaintained and its Lambda layer keeps breaking on new runtime versions. If what you actually need is form-filling rather than HTML rendering, the migration is smaller than it looks — no headless anything required. Notes from doing it. #webdev
pypdf will silently leave a checkbox unchecked even after you set its value, because checkbox state lives in two places (the field value and the widget's appearance state) and most code only sets one. Root cause and the two-line fix. #webdev
Filling out a government PDF that's been encrypted by the issuer (some W-2 and benefits forms ship this way) breaks most fill libraries before they even parse the form. Wrote up what the encryption actually restricts and how to fill around it. #webdev
A monthly report that's the same layout with new numbers doesn't need a rendering engine — it needs the same template filled with this month's data. Use case walkthrough for reports built from a fixed PDF template. #webdev
pdfshift and most hosted PDF APIs are HTML-to-PDF under the hood — you write a template in HTML, they screenshot it to a PDF via a browser. If you're filling an existing form instead of generating a new document, that's the wrong primitive. Comparison →
Two runs, same template, same field values, different output bytes — that's most PDF APIs, because of a stamped creation date or a random object ID. We picked apart what has to be zeroed to make the output actually deterministic. #webdev
Snapshot-testing a PDF in CI fails constantly if the generator embeds a timestamp or a random ID — every run looks like a diff. Byte-identical output means the snapshot actually catches regressions instead of just churning. #webdev
Giving an LLM agent a PDF-filling tool over MCP means one call it can't get wrong: send a field map, get bytes back, no browser session to babysit and no binary for the agent's sandbox to lack. Wrote up the tool schema. #webdev
Merging PDFs with mixed page sizes (a cover letter plus scanned exhibits) breaks naive concatenation — pages get cropped or stretched to match the first doc. Wrote up how to preserve each page's own dimensions on merge.
Federal court forms (AO 440 and friends) ship as AcroForms with field names that don't resemble the printed labels at all. Registry entry has the real names, no PDF viewer required.
IRS W-4 has fields that don't match its printed labels one-to-one — some are split across sub-fields for multi-jobs. The registry entry lists the real internal names before you write the fill call.
IRS W-9 field names, mapped in advance: registry entry lists every AcroForm field on the current revision so you can build the {field: value} map without opening the PDF in a viewer first.
the registry lists the AcroForm field names for common government forms (W-9, I-9, WH-347, Form 1040...) so you don't have to paste each one into the Inspector yourself first. Useful if you're filling the same handful of standard forms on repeat. #webdev
invoice, contract, report — three PDF use cases that look identical from a 'fill a template' view but differ in what has to be true afterward: an invoice needs deterministic bytes for accounting, a contract needs the fields flattened, a report just needs to merge cleanly. Breakdown →
pdf-lib runs client-side and in Workers, which makes it the closest thing to pdfops on paper — the difference shows up once you need the SAME output byte-for-byte across two different requests. Comparison with actual field-level notes →
wkhtmltopdf is unmaintained and its Docker image is the reason half of 'serverless PDF' Lambdas ship a 300MB layer. If the migration target is filling an existing form rather than rendering HTML, you don't need a replacement renderer — you need to stop rendering. Migration notes →
audit trail on a filled PDF usually means a separate log table joined to a document ID after the fact. We tried baking a hash of the field map straight into the PDF's own metadata at fill time instead — the proof travels with the file. Notes from shipping it in Next.js →
Giving an MCP-connected agent a PDF tool is different from giving a human one: the agent can't eyeball a form to find field names, so the tool has to expose inspect-then-fill as two calls it can chain itself. We wrote up the interface that worked. #webdev
Snapshot-testing PDFs in CI fails constantly if you hash the raw bytes — mod date, /ID, and object order change on every build even when nothing visible did. Strip those three fields before hashing and the snapshot actually catches real regressions instead of noise. #webdev
A fulfillment webhook fires, you need a packing slip PDF attached to the reply within the request timeout — no queue, no background job. That means the fill has to finish in the same invocation as the webhook handler. Here's the shape that keeps it under a Lambda's timeout. #serverless
Cron job that builds monthly PDF statement bundles: pull N records, fill N templates, merge into one file, upload. The part that usually breaks at scale isn't the fill, it's holding N filled PDFs in memory before the merge. Streaming the merge fixes it. #webdev
Deno Deploy has no filesystem and no node_modules — so a KYC packet pipeline (fill, stamp, merge, ship) has to run on pure-JS/WASM PDF primitives or it doesn't run at all. We built the whole flow on one HTTP call per step. Walkthrough →
Encrypted government PDFs (think W-9 variants some agencies ship pre-locked) reject most fillers outright because the crypt filter runs before any field write. You either decrypt-fill-reencrypt or use a lib that speaks the encryption dictionary natively. We wrote up the actual decision tree. #webdev
AcroForm checkboxes have two independent states: /V in the field dictionary and /AS on the widget. Set one without the other and the box reads checked in your code but blank in Preview. pypdf's set_need_appearances_writer papers over it; knowing both fields actually fixes it.
New on the blog: Merging a Letter invoice with an A4 attachment in pypdf, and the page size nobody normalizes. pypdf's append() copies each input page's own /MediaBox into the merged output as-is, so a Letter invoice merged with an A4 attachment comes out with two page sizes. Why →
The fill endpoint takes multipart/form-data, not a JSON body. In Python that's the PDF in files= and the field map in data= as json.dumps(fields), a JSON string, not a dict. Get that shape right and the rest is one requests.post. Working snippet →