<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: PDFops</title>
    <description>The latest articles on DEV Community by PDFops (@pdfops).</description>
    <link>https://dev.to/pdfops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982206%2F4660bc3b-b786-4bda-974f-3ad3af86fbea.png</url>
      <title>DEV Community: PDFops</title>
      <link>https://dev.to/pdfops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pdfops"/>
    <language>en</language>
    <item>
      <title>Migrating off wkhtmltopdf on AWS Lambda</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:07:58 +0000</pubDate>
      <link>https://dev.to/pdfops/migrating-off-wkhtmltopdf-on-aws-lambda-3bi8</link>
      <guid>https://dev.to/pdfops/migrating-off-wkhtmltopdf-on-aws-lambda-3bi8</guid>
      <description>&lt;p&gt;wkhtmltopdf's GitHub repo was archived in January 2023, its organization followed in July 2024, and its last release still carries an unpatched, CVSS-9.8 server-side request forgery. None of that has stopped it from running inside a Lambda Layer behind thousands of invoice and receipt endpoints — because "swap out the rendering engine" sounds like a multi-week project. Usually it isn't. If what you're generating is the same layout with different data every time, the fix is smaller than a rendering migration: stop rendering HTML and fill a form instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of wkhtmltopdf, plainly
&lt;/h2&gt;

&lt;p&gt;Three facts, so you can decide how urgent this is without taking my word for it:&lt;/p&gt;

&lt;p&gt;None of this is a knock on the original project — it did its job for over a decade. But "archived + unpatched critical CVE" is the point where a security review stops asking "is this still fine" and starts asking "why is this still here."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's worse specifically on Lambda
&lt;/h2&gt;

&lt;p&gt;wkhtmltopdf isn't a library — it's a compiled binary wrapping a decade-old fork of QtWebKit, which means running it in a function means shipping the binary alongside your code, not importing a package:&lt;/p&gt;

&lt;h2&gt;
  
  
  When this migration doesn't apply
&lt;/h2&gt;

&lt;p&gt;Be honest about what you're actually rendering before doing this. PDFops fills fields in a PDF template — it doesn't render arbitrary HTML and CSS. If your output genuinely varies in layout — long-form articles, portfolio pages, dashboards with charts, anything where the &lt;em&gt;structure&lt;/em&gt; changes per document, not just the values — you still need a real browser renderer. That's a different migration (Puppeteer or Playwright in a proper sandbox, not a Lambda Layer binary), and this guide isn't it.&lt;/p&gt;

&lt;p&gt;But check what you're actually generating. Most wkhtmltopdf endpoints render the &lt;em&gt;same&lt;/em&gt; HTML template every time — an invoice, a receipt, a monthly statement, a contract — with only the data changing. That's a form problem wearing an HTML renderer's clothes, and it's the case this guide covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The swap
&lt;/h2&gt;

&lt;p&gt;Re-author the HTML template once as a real PDF with AcroForm fields — where a Liquid/Handlebars variable used to sit, there's now a named form field. Acrobat's Prepare Form tool, LibreOffice Draw's Form Controls toolbar, or &lt;code&gt;pdftk&lt;/code&gt; on an existing PDF all work; it's a one-time job per template, not a per-request cost. Don't have one handy to test against? &lt;a href="https://pdfops.dev/invoice-template.pdf" rel="noopener noreferrer"&gt;Grab the sample invoice-template.pdf&lt;/a&gt; — it ships with &lt;code&gt;customer_name&lt;/code&gt; and &lt;code&gt;total&lt;/code&gt; fields already wired up.&lt;/p&gt;

&lt;p&gt;Once the template has real fields, &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;POST /api/fill-form&lt;/a&gt; replaces the entire render step: upload the template plus a JSON object of field values, get back a filled PDF. No HTML string assembly, no CSS, no rendering engine — just data going into boxes that already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lambda handler
&lt;/h2&gt;

&lt;p&gt;Node.js 22.x runtime, ESM, no Layer. The template ships inside the deployment zip and is read once at cold start — outside the handler — so a warm invocation does zero filesystem work before calling the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.mjs — AWS Lambda (Node.js 22.x), Function URL&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PdfOpsError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfops-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// Bundled in the deployment package — no Layer, no /tmp write.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice-template.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDFOPS_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST a JSON body: { customer_name, total }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filled&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;isBase64Encoded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PdfOpsError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm install pdfops-sdk&lt;/code&gt;, zip &lt;code&gt;index.mjs&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, and &lt;code&gt;invoice-template.pdf&lt;/code&gt; together, set &lt;code&gt;PDFOPS_API_KEY&lt;/code&gt; as an environment variable, done. Compare that deployment package — a handful of KB of pure JavaScript — to a Lambda Layer carrying a 50-80MB native binary, and the size difference alone is most of why cold starts get faster: there's no subprocess to spin up before your code even runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get beyond removing the CVE
&lt;/h2&gt;

&lt;p&gt;Filling a form is a narrower operation than rendering HTML, and narrower means more predictable. The same input bytes and the same field values produce byte-identical output every time — no font-substitution drift, no headless-renderer version bump silently reflowing last month's invoices. If that determinism matters for hashing, caching, or diffing PDFs in CI, that's the whole argument in &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;the case for deterministic PDF filling&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a key
&lt;/h2&gt;

&lt;p&gt;The handler above needs a key to run past the anonymous trial. &lt;a href="https://pdfops.dev/docs/signup" rel="noopener noreferrer"&gt;POST /api/signup&lt;/a&gt; with an email gets a free one — 250 requests/month, no card, delivered to your inbox (never in the response body, so inbox possession doubles as ownership proof):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"you@example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anonymous calls (no &lt;code&gt;X-API-Key&lt;/code&gt; header) still work at 100 requests/IP/month — enough to run the example below before deciding whether to wire in a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Prove the fill step from a terminal before touching Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/fill-form &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@invoice-template.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'fields={"customer_name":"Acme Co","total":"$1,250.00"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; filled.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;filled.pdf&lt;/code&gt; opens with both fields populated — that curl call is exactly what the Lambda handler above does under the hood, just wrapped in the SDK's typed &lt;code&gt;fillForm&lt;/code&gt; method instead of raw multipart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>serverless</category>
      <category>api</category>
    </item>
    <item>
      <title>Assembling a KYC packet on Deno Deploy</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Tue, 21 Jul 2026 21:12:02 +0000</pubDate>
      <link>https://dev.to/pdfops/assembling-a-kyc-packet-on-deno-deploy-39go</link>
      <guid>https://dev.to/pdfops/assembling-a-kyc-packet-on-deno-deploy-39go</guid>
      <description>&lt;p&gt;Compliance owns the KYC application template, not engineering — and they revise it without filing a ticket. Hardcode the field names once and the next template swap either breaks silently (a field goes unfilled) or throws &lt;code&gt;unknown_field&lt;/code&gt; in production. The fix is to stop assuming the shape of a PDF you don't control: ask it, at request time, what fields it actually has, fill whatever comes back, then merge the filled application with the applicant's uploaded ID scan into one packet. Here's the whole flow in ~45 lines on Deno Deploy, using the new &lt;a href="https://www.npmjs.com/package/pdfops-sdk" rel="noopener noreferrer"&gt;pdfops-sdk&lt;/a&gt; instead of raw &lt;code&gt;fetch&lt;/code&gt; calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why inspect first
&lt;/h2&gt;

&lt;p&gt;Every other example on this blog hardcodes a JSON object of field names — reasonable when the template PDF is a file you committed to your own repo. A KYC application template isn't that. It's owned by a compliance or ops team who edit it in Acrobat, rename a field, add a new disclosure checkbox, and re-upload it to wherever your app fetches it from — all without a code review on your side. Filling against a hardcoded key list against a template you don't control is a bug waiting for the next revision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pdfops.dev/docs/inspect" rel="noopener noreferrer"&gt;POST /api/inspect&lt;/a&gt; answers "what fields does &lt;em&gt;this&lt;/em&gt; PDF have right now?" — every AcroForm field's name, type, and (for dropdowns/radios) its options, plus a ready-to-fill &lt;code&gt;fillTemplate&lt;/code&gt; object keyed by name. Call it once per request against the live template, and the fill step only ever writes into fields that actually exist. A template revision changes what gets filled, not whether the request throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the SDK, get a key
&lt;/h2&gt;

&lt;p&gt;Every earlier post here uses &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;FormData&lt;/code&gt; directly against the HTTP API — that still works and always will. &lt;code&gt;pdfops-sdk&lt;/code&gt; is a thin typed wrapper around the same endpoints: no dependencies, edge-safe (Workers, Vercel Edge, Deno, Bun, Node 18+, browsers), and it turns the multipart plumbing into three method calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;pdfops-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need a key to run more than the anonymous trial. A free one costs nothing and needs no card — email in, key out (the key arrives by email, not in the response body, so inbox possession is the verification step):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PdfOps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfops-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// no key: 100 requests/IP/month&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;you@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// free key, 250/month, no card — check your inbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the key lands, wire it in and every call meters against your own quota instead of your IP's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PDFOPS_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Deno Deploy handler
&lt;/h2&gt;

&lt;p&gt;The endpoint takes a multipart POST — the applicant's data as a JSON field, their ID scan as a file — pulls the current application template, inspects it, fills only the fields it finds, and merges in the ID scan. No browser, no second runtime, ~45 lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts — Deno Deploy&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PdfOpsError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfops-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfOps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PDFOPS_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST multipart/form-data: applicant, id_scan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idScan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id_scan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applicantRaw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applicant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idScan&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;applicantRaw&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expected fields "applicant" (JSON) and "id_scan" (file)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applicantRaw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Pull the CURRENT template — compliance swaps this file on their&lt;/span&gt;
  &lt;span class="c1"&gt;//    own schedule, so the fetch (not a bundled asset) is deliberate.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templateResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TEMPLATE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;templateResp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Ask the template what fields it has right now, instead of&lt;/span&gt;
  &lt;span class="c1"&gt;//    assuming a fixed shape.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fillTemplate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Map applicant data onto whatever fields actually exist. A field&lt;/span&gt;
  &lt;span class="c1"&gt;//    the template dropped is silently skipped; a field the applicant&lt;/span&gt;
  &lt;span class="c1"&gt;//    didn't send stays at the template's default.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fillTemplate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 4. Fill the application.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Merge the filled application with the uploaded ID scan into&lt;/span&gt;
    &lt;span class="c1"&gt;//    one packet — page order follows array order.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;idScan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;filled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idBytes&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PdfOpsError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deployctl deploy main.ts&lt;/code&gt;, set &lt;code&gt;PDFOPS_API_KEY&lt;/code&gt; and &lt;code&gt;TEMPLATE_URL&lt;/code&gt; as environment variables, and you have a live endpoint. The &lt;code&gt;fillForm&lt;/code&gt; and &lt;code&gt;merge&lt;/code&gt; calls both return &lt;code&gt;Uint8Array&lt;/code&gt; PDF bytes — hand them to storage, a &lt;code&gt;Response&lt;/code&gt;, or an email attachment as-is; nothing in this handler buffers more than one document at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why merge goes last, and what else it can hold
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;merge([filled, idBytes])&lt;/code&gt; concatenates pages in array order, so the filled application always lands as page 1 — the reviewer opens the packet and reads the application first, attachments after. The array isn't capped at two: a real KYC packet is usually &lt;code&gt;merge([filled, idFront, idBack, proofOfAddress])&lt;/code&gt; — every attachment the applicant uploaded, in the order you want a reviewer to see them, still one &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;POST /api/merge&lt;/a&gt; call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors and watching your quota
&lt;/h2&gt;

&lt;p&gt;Every non-2xx response the SDK sees throws a &lt;code&gt;PdfOpsError&lt;/code&gt; with the API's stable error slug — the &lt;code&gt;catch&lt;/code&gt; block above turns &lt;code&gt;unknown_field&lt;/code&gt;, &lt;code&gt;invalid_pdf&lt;/code&gt;, or a &lt;code&gt;429 rate_limited&lt;/code&gt; straight into the right HTTP status for the caller, instead of a generic 500. And because the free tier has a real ceiling (250/month per key), check it before you're surprised by one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resets_at&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;a href="https://pdfops.dev/docs/usage" rel="noopener noreferrer"&gt;GET /api/usage&lt;/a&gt; under the hood — it reads the same counter the 429 fires on, so the number you show a user is never out of sync with the number that blocks them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits a real app
&lt;/h2&gt;

&lt;p&gt;Swap the runtime and the trigger, and the same three-call shape — inspect, fill, merge — covers most "combine a filled form with something a user uploaded" flows:&lt;/p&gt;

&lt;p&gt;The part that's new here is &lt;code&gt;inspect&lt;/code&gt; in the chain — reach for it whenever the template isn't a file you fully control, not just for KYC packets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;All three endpoints are live. Prove the chain from your terminal before writing a line of Deno:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. What fields does this template have?&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/inspect &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@application.pdf"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Fill it&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/fill-form &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@application.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'fields={"applicant_name":"Ada Lovelace","country":"UK"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; filled.pdf

&lt;span class="c"&gt;# 3. Merge with the ID scan&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@filled.pdf"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@id-scan.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; packet.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get 100 keyless requests per IP per month, or &lt;code&gt;pdfops.signup('you@example.com')&lt;/code&gt; / the form at &lt;a href="https://pdfops.dev/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt; for a free key (250/mo, no card — delivered by email).&lt;/p&gt;

&lt;p&gt;Building a KYC or onboarding flow and something about &lt;code&gt;inspect&lt;/code&gt;'s field-type coverage doesn't fit yours? &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;Drop a note on the feedback form&lt;/a&gt; — it's the fastest way to influence what ships next.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>serverless</category>
      <category>api</category>
    </item>
    <item>
      <title>Fill a PDF form inside a Cloudflare Worker — no Chromium, no Lambda</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Tue, 30 Jun 2026 22:50:40 +0000</pubDate>
      <link>https://dev.to/pdfops/fill-a-pdf-form-inside-a-cloudflare-worker-no-chromium-no-lambda-2bpc</link>
      <guid>https://dev.to/pdfops/fill-a-pdf-form-inside-a-cloudflare-worker-no-chromium-no-lambda-2bpc</guid>
      <description>&lt;p&gt;You're building on Cloudflare Workers and you need to write values into a PDF form — an invoice, a contract, a government form. You reach for the thing you used last time: headless Chrome, or a PDF library bundled into a Lambda. On Workers, neither one fits. Chrome won't run in a V8 isolate, and standing up a Lambda just to render a PDF drags a whole second runtime into an app that didn't need one. The fill itself is one HTTP call. Here's the minimal Worker, and the reason the hosting substrate — not the PDF code — is the part that actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why headless Chrome doesn't fit Workers
&lt;/h2&gt;

&lt;p&gt;The default way to "make a PDF in JavaScript" for the last decade has been Puppeteer driving headless Chrome: render HTML, call &lt;code&gt;page.pdf()&lt;/code&gt;, done. It works on a normal Node server or a fat Lambda with a Chromium layer. It does not work on Cloudflare Workers, and the reason is architectural, not a missing flag. A Worker runs in a &lt;strong&gt;V8 isolate&lt;/strong&gt; — the same engine Chrome uses, but with no operating system underneath it. There's no filesystem, no ability to spawn a subprocess, and a hard memory and CPU-time budget per request. Headless Chrome is an entire browser binary that expects all of those things. You can't bundle a ~150 MB browser into a Worker, and even if you could, there's nothing to &lt;code&gt;exec()&lt;/code&gt; it.&lt;/p&gt;

&lt;p&gt;AcroForm filling — writing values into a form-enabled PDF's existing fields — doesn't need a browser at all. The fields are already defined in the PDF; you're setting their values and flattening, which is pure byte manipulation. The work is a poor match for a render engine and a perfect match for a stateless function. The only question is &lt;em&gt;where that function runs&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just put it on Lambda
&lt;/h2&gt;

&lt;p&gt;The usual escape hatch is "keep the PDF work on AWS Lambda and call it from the Worker." That works, but look at what it costs you. You're now running two runtimes for one feature: the Worker that owns the request, and a Lambda that exists only to hold a PDF library. You inherit Lambda's cold starts on a path your edge app was specifically built to keep fast. You're routing edge → &lt;code&gt;us-east-1&lt;/code&gt; → edge for every document, so a user in Sydney pays a trans-Pacific round trip to fill a one-page form. And you've split your deploy: two log streams, two IAM surfaces, two things to keep in sync. None of that is about PDFs. It's all substrate drag, bolted onto an app that chose Workers to avoid exactly this.&lt;/p&gt;

&lt;p&gt;The alternative is to treat the fill as what it is — a stateless transform — and call a hosted endpoint that runs on the same kind of globally-distributed substrate your Worker already lives on. The Worker stays the only thing you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worker
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing. It takes a JSON body of field values, fetches an AcroForm template from R2, calls &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;/api/fill-form&lt;/a&gt;, and returns the filled PDF. No browser, no second runtime, ~35 lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/index.ts — Cloudflare Worker (module syntax)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;TEMPLATES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;R2Bucket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// bucket holding your blank AcroForm PDFs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST a JSON body of field values&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. The values to write into the form's named fields.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Pull the blank template from R2 (cached at the edge after first read).&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEMPLATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice-template.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;template missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templatePdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. One call to PDFops. Field keys must match the PDF's AcroForm&lt;/span&gt;
    &lt;span class="c1"&gt;//    field names — use /tools/inspect to list them if you're unsure.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;templatePdf&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;template.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pdfops.dev/api/fill-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`fill failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Stream the filled PDF straight back to the caller.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the production shape. Bind an R2 bucket named &lt;code&gt;TEMPLATES&lt;/code&gt; in &lt;code&gt;wrangler.toml&lt;/code&gt;, drop a form-enabled PDF into it, &lt;code&gt;wrangler deploy&lt;/code&gt;, and POST a JSON object of field values. You get a filled PDF back with no second service in the picture. The &lt;code&gt;resp.body&lt;/code&gt; stream means the Worker never buffers the whole document in memory — it pipes PDFops' response through, which keeps you well inside the isolate's memory budget even for large forms.&lt;/p&gt;

&lt;p&gt;Two things worth knowing. The field &lt;em&gt;keys&lt;/em&gt; have to match the names baked into the PDF's AcroForm — if you're not sure what they are, the free &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Form-Field Inspector&lt;/a&gt; lists every field name in any PDF you drop on it. And the R2 &lt;code&gt;get()&lt;/code&gt; is edge-cached after the first read, so you're not re-fetching the template on every request — the steady-state path is just the one &lt;code&gt;fetch&lt;/code&gt; to fill-form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits a real app
&lt;/h2&gt;

&lt;p&gt;The bare Worker above is the primitive. In practice the trigger is usually a webhook or a queue, and the output goes to storage plus an email. Those are the same pattern with more wiring around the fill step:&lt;/p&gt;

&lt;p&gt;In every one of these the fill is the same single &lt;code&gt;fetch&lt;/code&gt;. What changes is the trigger and the destination — never the PDF substrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this pattern doesn't fit
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The endpoint is live and works against any AcroForm PDF. Before you even write the Worker, prove the fill from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;X&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//pdfops.dev/api/fill-form \&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf=@invoice-template.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields={"customer_name":"Acme Corp","invoice_no":"INV-1042","amount_due":"$2,400.00"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="nx"&gt;filled&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pdf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get the filled PDF back. From there the Worker above is just that same call wrapped in a &lt;code&gt;fetch&lt;/code&gt; handler. During beta it's 100 requests per IP per month, free, no signup.&lt;/p&gt;

&lt;p&gt;Workers-specific questions, a binding that's fighting you, or an endpoint you wish existed? &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;Drop a note on the waitlist form&lt;/a&gt; — the message field is the fastest way to influence what ships next.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>pdf</category>
    </item>
    <item>
      <title>Fill a PDF in JavaScript — in the browser or via API</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Sat, 13 Jun 2026 18:03:40 +0000</pubDate>
      <link>https://dev.to/pdfops/fill-a-pdf-in-javascript-in-the-browser-or-via-api-4ej</link>
      <guid>https://dev.to/pdfops/fill-a-pdf-in-javascript-in-the-browser-or-via-api-4ej</guid>
      <description>&lt;p&gt;JavaScript gives you a choice no other language quite does: you can fill a PDF &lt;strong&gt;entirely in the browser&lt;/strong&gt;, so the file never leaves the user’s machine, or you can fill it &lt;strong&gt;server-side&lt;/strong&gt; when the output must be authoritative. This page shows both with runnable code, and is honest about when each is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the browser, client-side (the file never uploads)
&lt;/h2&gt;

&lt;p&gt;With &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;pdf-lib&lt;/a&gt; the whole fill runs in the page. Read a chosen file into bytes, set the AcroForm fields, and offer the result as a download — nothing is sent anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;input type="file"&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Co&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1,250.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// form.flatten();  // optional: bake values in&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filled.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the right default for anything sensitive — tax forms, contracts, medical intake — because the document stays on the device. You can see exactly this pattern, fill &lt;em&gt;and&lt;/em&gt; merge, running live in the &lt;a href="https://pdfops.dev/playground" rel="noopener noreferrer"&gt;PDFops playground&lt;/a&gt;, which does all its work client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-side, via one &lt;code&gt;fetch&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When the filled PDF needs to be authoritative — generated from data the browser doesn’t have, or produced where the client can’t be trusted to make the canonical copy — fill it through the API. The same &lt;code&gt;fetch&lt;/code&gt; works from a Worker, an edge function, or a Node backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileBlob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// a Blob/File of the template&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Co&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;invoice_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1,250.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://pdfops.dev/api/fill-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// the filled PDF bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, call this from &lt;strong&gt;your own backend or edge function&lt;/strong&gt; rather than directly from the browser — that keeps rate limits and keys (post-beta) under your control and out of end-user hands. During beta there’s no key, so a direct browser call is fine for a prototype. Use the &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Form-Field Inspector&lt;/a&gt; to get the exact field names for your &lt;code&gt;fields&lt;/code&gt; object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser or server: how to choose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Client-side (pdf-lib in browser)&lt;/th&gt;
&lt;th&gt;Server-side (PDFops API)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File leaves the device&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — stays in the browser&lt;/td&gt;
&lt;td&gt;Yes — sent to the API to fill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoritative output&lt;/td&gt;
&lt;td&gt;Client-produced (trust the client)&lt;/td&gt;
&lt;td&gt;Server-produced (canonical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fill from server-only data&lt;/td&gt;
&lt;td&gt;No — only what the page has&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form internals you manage&lt;/td&gt;
&lt;td&gt;Typed accessors, flatten, appearances&lt;/td&gt;
&lt;td&gt;None — handled server-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge available&lt;/td&gt;
&lt;td&gt;Yes — pdf-lib copyPages&lt;/td&gt;
&lt;td&gt;Yes — &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;/api/merge&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;Deterministic (no AI)&lt;/td&gt;
&lt;td&gt;Deterministic, audit-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Privacy-first, zero-upload, instant preview&lt;/td&gt;
&lt;td&gt;Canonical records, server data, store/sign/merge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest summary: if privacy or zero-upload is the point, &lt;strong&gt;fill client-side with pdf-lib&lt;/strong&gt; — it’s free and the file never moves. If the output must be the system-of-record copy, or you fill from data the browser doesn’t hold, fill server-side. A common shape is both: a fast client-side preview, then a server-side canonical fill on submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging PDFs in JavaScript too
&lt;/h2&gt;

&lt;p&gt;Server-side, merge is the same one-call primitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pdfBlobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdfs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://pdfops.dev/api/merge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client-side, pdf-lib merges with &lt;code&gt;copyPages&lt;/code&gt; into a fresh document. Either way the fill-then-merge flow stays deterministic end to end. Endpoint details: &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;fill-form docs&lt;/a&gt;, &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;merge docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I fill a PDF form in the browser with JavaScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Read the file into an ArrayBuffer, then use pdf-lib client-side: load the bytes, getForm(), set field values, save() back to bytes you offer as a download. Everything runs in the browser, so the PDF never leaves the device. The &lt;a href="https://pdfops.dev/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; does exactly this, live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I fill in the browser or on the server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browser when privacy or zero-upload matters — the file stays on the device. Server when the output must be authoritative, when you fill from data the browser lacks, or when you also store/sign/merge server-side. Many apps do both: client-side preview, server-side canonical fill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I call the PDFops API from browser JavaScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — it's a normal fetch with FormData. For production, call it from your own backend or edge function so you control rate limits and keys (post-beta) and don't expose usage to end users. During beta there's no key, so a direct browser call works for prototypes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is pdf-lib enough to fill a PDF in JavaScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Often yes — pdf-lib is pure JS, runs in browsers and Node, and fills AcroForm fields well. You own the form internals (typed accessors, flatten, appearance/font edge cases). PDFops is built on a modernized pdf-lib fork and offers that engine as a deterministic hosted API for when you'd rather not own those edge cases or need server-authoritative output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does filling change the bytes unpredictably?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It shouldn't. Both pdf-lib and the PDFops API fill deterministically — same template plus same values yields the same field-level output, no AI in the path. Outputs stay diffable and audit-safe. The argument is in &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;this essay&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Fill and merge a real PDF client-side, right now, in the &lt;a href="https://pdfops.dev/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; — no signup. On another stack? See &lt;a href="https://pdfops.dev/fill-pdf-in-python" rel="noopener noreferrer"&gt;fill a PDF in Python&lt;/a&gt; and &lt;a href="https://pdfops.dev/fill-pdf-in-node" rel="noopener noreferrer"&gt;fill a PDF in Node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the deterministic fill + merge primitive fits your usage, &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt; and tell me the forms you fill most — that signal is what the pricing tiers and the in-function library get built around.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Fill a PDF in Node.js — one fetch, no headless browser</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Sat, 13 Jun 2026 17:57:32 +0000</pubDate>
      <link>https://dev.to/pdfops/fill-a-pdf-in-nodejs-one-fetch-no-headless-browser-5h4d</link>
      <guid>https://dev.to/pdfops/fill-a-pdf-in-nodejs-one-fetch-no-headless-browser-5h4d</guid>
      <description>&lt;p&gt;To fill a PDF form from Node you can run it in-process with &lt;strong&gt;pdf-lib&lt;/strong&gt;, or POST the template to an HTTP API and get the filled PDF back. On &lt;strong&gt;Node 18+&lt;/strong&gt; the HTTP path needs zero dependencies — &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;FormData&lt;/code&gt; are global — and the same code runs unchanged on Workers, Lambda, and edge functions. Here is both, with an honest read on which fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fastest path: one &lt;code&gt;fetch&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PDFops fills &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;AcroForm fields&lt;/a&gt; server-side. From modern Node it is a few lines, no packages installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Co&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;invoice_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1,250.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://pdfops.dev/api/fill-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`fill failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filled.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keys in the &lt;code&gt;fields&lt;/code&gt; object must match the AcroForm field names in the template. Run the PDF through the &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Form-Field Inspector&lt;/a&gt; to see the exact names and types. No API key or signup during beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it in-process: pdf-lib
&lt;/h2&gt;

&lt;p&gt;The local route is pure JavaScript and keeps everything in-process — no network call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;template.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Co&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1,250.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// form.flatten();  // optional: bake values in, drop interactivity&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filled.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a genuinely good library and for many backends it is the right answer. The work you take on is the form internals: getting the right typed accessor per field (&lt;code&gt;getTextField&lt;/code&gt; vs &lt;code&gt;getCheckBox&lt;/code&gt; vs &lt;code&gt;getRadioGroup&lt;/code&gt;), deciding when to &lt;code&gt;flatten()&lt;/code&gt;, and handling appearance/font cases on unusual templates. PDFops is built on a &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;modernized fork of pdf-lib&lt;/a&gt;, so the API path is essentially that same engine offered as a managed, deterministic service — you trade a network call for not owning the edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters on serverless and the edge
&lt;/h2&gt;

&lt;p&gt;The reason the &lt;code&gt;fetch&lt;/code&gt; version is interesting isn’t brevity — it’s portability. Because the call is plain &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;FormData&lt;/code&gt;, both Web-standard, &lt;strong&gt;the identical code runs on Cloudflare Workers, Vercel Edge, Deno Deploy, and Bun&lt;/strong&gt;, not only Node. There is no headless Chromium to bundle and no native addon to compile, which is exactly what tends to block PDF tooling on those platforms.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PDFops (HTTP)&lt;/th&gt;
&lt;th&gt;pdf-lib (in-process)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies on Node 18+&lt;/td&gt;
&lt;td&gt;None — global fetch/FormData&lt;/td&gt;
&lt;td&gt;pdf-lib (pure JS, no native addons)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs on Workers / Edge / Deno / Bun&lt;/td&gt;
&lt;td&gt;Yes — same code, unchanged&lt;/td&gt;
&lt;td&gt;Yes — pure JS, but you bundle it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network call required&lt;/td&gt;
&lt;td&gt;Yes — one POST per fill&lt;/td&gt;
&lt;td&gt;No — fully in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form internals you manage&lt;/td&gt;
&lt;td&gt;None — handled server-side&lt;/td&gt;
&lt;td&gt;Typed accessors, flatten, appearances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge in the same tool&lt;/td&gt;
&lt;td&gt;Yes — &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;/api/merge&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Yes — copyPages + save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;Deterministic, audit-safe&lt;/td&gt;
&lt;td&gt;Deterministic (no AI either)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;No-dependency serverless, fill+merge as a service&lt;/td&gt;
&lt;td&gt;In-process control, no-network constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest summary: if you want the fill fully in-process and don’t mind owning form internals, &lt;strong&gt;pdf-lib is an excellent, free choice&lt;/strong&gt;. If you want one deterministic API for fill and merge, identical behavior from local Node to the edge, and nothing to bundle or compile, the HTTP call is worth the round-trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging PDFs from Node too
&lt;/h2&gt;

&lt;p&gt;The same primitive merges several PDFs into one in a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdfs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://pdfops.dev/api/merge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merged.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common backend shape is fill-then-merge: fill several templates, then concatenate the results into one document to deliver. Both halves are the same deterministic primitive, so the combined output is reproducible. Endpoint details: &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;fill-form docs&lt;/a&gt;, &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;merge docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I fill a PDF form in Node.js?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Either run pdf-lib in-process to set AcroForm values, or build a FormData with the template plus a JSON field map and POST it. On Node 18+, fetch/FormData/Blob are global, so the HTTP fill needs no dependencies and runs unchanged on Workers, Lambda, and edge functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use pdf-lib or PDFops?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pdf-lib when you want the fill fully in-process with no network call and are comfortable managing form internals. PDFops when you want one deterministic API for fill and merge, identical behavior across Node and edge, and nothing to bundle. PDFops is built on a modernized pdf-lib fork, so the API gives you that engine as a managed service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I fill a PDF on Cloudflare Workers or Vercel Edge?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the call is plain fetch + FormData, both Web-standard, so the same code runs on Workers, Vercel Edge, Deno Deploy, and Bun. No headless browser, no native addon — which is the usual blocker for PDF tooling on those platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a library to call PDFops from Node?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. On Node 18+, fetch/FormData/Blob are global, so a fill is a few lines with zero dependencies. On older Node, add undici or node-fetch plus form-data, or just upgrade the runtime. There is no SDK to install during beta — the API is plain HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the fill deterministic and audit-safe?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — same template plus same field values yields the same field-level output, no AI inference in the path. Outputs are diffable and reproducible, which matters when a filled PDF is a record you may need to defend. The argument is in &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;this essay&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;No API key, no signup during beta. Paste the snippet above into a Node script, or explore fields in the &lt;a href="https://pdfops.dev/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt;. On another stack? See &lt;a href="https://pdfops.dev/fill-pdf-in-python" rel="noopener noreferrer"&gt;fill a PDF in Python&lt;/a&gt; and &lt;a href="https://pdfops.dev/fill-pdf-in-javascript" rel="noopener noreferrer"&gt;fill a PDF in JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the deterministic fill + merge primitive fits your usage, &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt; and tell me the forms you fill most — that signal is what the pricing tiers and the in-function library get built around.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>api</category>
    </item>
    <item>
      <title>Fill a PDF in Python — one HTTP call, no native deps</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Sat, 13 Jun 2026 17:57:12 +0000</pubDate>
      <link>https://dev.to/pdfops/fill-a-pdf-in-python-one-http-call-no-native-deps-3jek</link>
      <guid>https://dev.to/pdfops/fill-a-pdf-in-python-one-http-call-no-native-deps-3jek</guid>
      <description>&lt;p&gt;If you need to fill a PDF form from Python, you have two honest options: drive a native library like &lt;strong&gt;pypdf&lt;/strong&gt; or &lt;strong&gt;fillpdf&lt;/strong&gt;, or POST the template to an HTTP API and get the filled PDF back. This page shows the HTTP path in full, then makes the case for &lt;em&gt;when&lt;/em&gt; the native library is the better call — because sometimes it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fastest path: one &lt;code&gt;requests.post&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PDFops fills &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;AcroForm fields&lt;/a&gt; server-side on the edge. From Python it is one request — no &lt;code&gt;pdftk&lt;/code&gt;, no &lt;code&gt;poppler&lt;/code&gt;, nothing to install beyond &lt;code&gt;requests&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pdfops.dev/api/fill-form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Co&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$1,250.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filled.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field names in the dict must match the AcroForm field names in the template. If you are not sure what those are, drop the PDF into the &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Form-Field Inspector&lt;/a&gt; — it lists every field name and type, so your keys line up on the first try. No API key or signup is required during beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it natively: pypdf and fillpdf
&lt;/h2&gt;

&lt;p&gt;The pure-Python route is real and worth knowing. &lt;strong&gt;pypdf&lt;/strong&gt; fills AcroForm fields without any system binaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PdfWriter&lt;/span&gt;

&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfWriter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_page_form_field_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Co&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$1,250.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;auto_regenerate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filled.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, and for many templates it is all you need. The friction shows up at the edges: some viewers won’t render filled values until you set the &lt;code&gt;NeedAppearances&lt;/code&gt; flag or regenerate appearance streams; checkboxes and radio groups need the exact on-state name, not &lt;code&gt;True&lt;/code&gt;; and embedded-font edge cases can drop characters. None of these are dealbreakers — they are simply PDF-internals work you now own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fillpdf&lt;/strong&gt; wraps &lt;code&gt;pdfrw&lt;/code&gt; with a friendlier API and can flatten, but flattening pulls in &lt;code&gt;pdftk&lt;/code&gt; (and often &lt;code&gt;poppler&lt;/code&gt;), which is the usual snag inside containers, Lambda, and other serverless runtimes where shipping system binaries is awkward.&lt;/p&gt;

&lt;h2&gt;
  
  
  When PDFops fits, when a local library fits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PDFops (HTTP)&lt;/th&gt;
&lt;th&gt;pypdf / fillpdf (local)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native dependencies&lt;/td&gt;
&lt;td&gt;None — just &lt;code&gt;requests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pypdf: none; fillpdf: pdftk/poppler to flatten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless / edge friendly&lt;/td&gt;
&lt;td&gt;Yes — nothing to provision&lt;/td&gt;
&lt;td&gt;pypdf: yes; fillpdf: painful (binaries)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network call required&lt;/td&gt;
&lt;td&gt;Yes — one POST per fill&lt;/td&gt;
&lt;td&gt;No — fully local/offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Appearance-stream quirks&lt;/td&gt;
&lt;td&gt;Handled server-side&lt;/td&gt;
&lt;td&gt;You own NeedAppearances, fonts, checkboxes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge in the same tool&lt;/td&gt;
&lt;td&gt;Yes — &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;/api/merge&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;pypdf can merge; fillpdf cannot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;Deterministic, audit-safe&lt;/td&gt;
&lt;td&gt;Deterministic (no AI either)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Serverless backends, no-binary stacks, fill+merge at scale&lt;/td&gt;
&lt;td&gt;Offline jobs, no-network constraints, full local control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest summary: if your code already runs somewhere you control with no network constraint and you don’t mind owning PDF internals, &lt;strong&gt;pypdf is a fine, free, dependency-light choice&lt;/strong&gt;. If you are on serverless/edge, want fill and merge behind one deterministic API, or simply don’t want to debug appearance streams, the HTTP call earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging PDFs in Python too
&lt;/h2&gt;

&lt;p&gt;The same primitive merges. Concatenate several PDFs into one in a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdfs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pdfops.dev/api/merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common shape is fill-then-merge: fill several AcroForm templates from your Python backend, then merge the results into one document to deliver. Both halves are the same deterministic primitive, so the combined output is reproducible end to end. The endpoint details are in &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;the fill-form docs&lt;/a&gt; and &lt;a href="https://pdfops.dev/docs/merge" rel="noopener noreferrer"&gt;the merge docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I fill a PDF form in Python?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Either drive a native library (pypdf, fillpdf) that writes AcroForm values from Python, or POST the template plus a JSON map of field values to an HTTP API and get the filled PDF back. The native route is fully local; the HTTP route needs no system binaries, which is why it suits serverless and edge runtimes where installing pdftk or poppler is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use pypdf or PDFops?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pypdf when you want zero network calls and full local control and are happy handling appearance-stream quirks yourself. PDFops when you are on serverless/edge with no easy way to ship binaries, want one deterministic API for fill &lt;em&gt;and&lt;/em&gt; merge, or would rather not own the PDF-internals edge cases. Many teams prototype with pypdf and move the hot path to the API once the quirks cost time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I fill a PDF in Python without pdftk or poppler?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. pypdf is pure Python and needs no binaries to fill. fillpdf needs pdftk/poppler to flatten, which is the usual container/serverless snag. The PDFops API needs nothing installed locally — the fill runs server-side — so it is a common pick exactly when pdftk and poppler are awkward to provision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the fill deterministic?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the same template plus the same field values yields the same field-level output, with no AI inference in the path. Outputs are diffable and audit-safe, which matters for regulated documents where identical inputs must produce identical PDFs. The longer argument is in &lt;a href="https://pdfops.dev/blog/deterministic-pdf-filling" rel="noopener noreferrer"&gt;this essay&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my PDF has no form fields?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Filling requires AcroForm fields to exist. If a template has none, add them once in Acrobat, Mac Preview, LibreOffice Draw, or pdftk, then fill that template repeatedly. The free &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Inspector&lt;/a&gt; lists the field names a PDF exposes so your Python dict keys match them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;No API key, no signup during beta. Fill a real template right now with the snippet above, or explore fields visually in the &lt;a href="https://pdfops.dev/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt;. Filling from another stack? See &lt;a href="https://pdfops.dev/fill-pdf-in-node" rel="noopener noreferrer"&gt;fill a PDF in Node&lt;/a&gt; and &lt;a href="https://pdfops.dev/fill-pdf-in-javascript" rel="noopener noreferrer"&gt;fill a PDF in JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the deterministic fill + merge primitive fits your usage, &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt; and tell me the forms you fill most — that signal is what the pricing tiers and the in-function library get built around.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The case for deterministic PDF filling</title>
      <dc:creator>PDFops</dc:creator>
      <pubDate>Sat, 13 Jun 2026 05:28:20 +0000</pubDate>
      <link>https://dev.to/pdfops/the-case-for-deterministic-pdf-filling-2oo0</link>
      <guid>https://dev.to/pdfops/the-case-for-deterministic-pdf-filling-2oo0</guid>
      <description>&lt;p&gt;AI can read almost any document now. The harder question is what&lt;br&gt;
writes the answer back — and for anything an auditor might ever&lt;br&gt;
look at, that write step should not be a language model.&lt;/p&gt;
&lt;h2&gt;
  
  
  A document workflow has two halves
&lt;/h2&gt;

&lt;p&gt;Most real document automation is a loop: &lt;strong&gt;read&lt;/strong&gt; data out of one document, then &lt;strong&gt;write&lt;/strong&gt; it into another. Read a scanned invoice, write the numbers into your ledger. Read an onboarding packet, write the values into a W-9. Read a claim, write an ACORD form.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;read&lt;/em&gt; half is having its moment. Vision-language models are genuinely good at pulling structured data out of messy, never-before-seen documents, and a wave of strong APIs — Extend, Reducto, LlamaParse, the hyperscalers’ document-AI services — have made it a solved-enough problem. If you need to understand an arbitrary PDF, reach for one of those.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;write&lt;/em&gt; half is a different problem with a different failure mode — and it’s the half people are quietly bolting an LLM onto because it’s adjacent. That’s the mistake.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why an LLM shouldn’t fill your W-9
&lt;/h2&gt;

&lt;p&gt;A model that fills a form “mostly” right is worse than useless on the documents that matter. It can misread a field label, conflate two values, or put the correct number in the wrong box. On a marketing one-pager, who cares. On a 1099, an insurance ACORD form, a healthcare pre-authorization, a tax filing — that’s not a typo, it’s a compliance incident.&lt;/p&gt;

&lt;p&gt;And here’s the part that doesn’t get said enough: &lt;strong&gt;if a filled value can’t be traced to a deterministic rule, it can’t be defended in an audit.&lt;/strong&gt; “The model was 97% confident” is not an answer when a regulator asks why field 14b says what it says. A probabilistic write step turns every filled form into something you have to &lt;em&gt;trust&lt;/em&gt; rather than &lt;em&gt;verify&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Determinism is a feature, not a limitation
&lt;/h2&gt;

&lt;p&gt;A deterministic fill is boring on purpose: field &lt;code&gt;customer_name&lt;/code&gt; maps to value &lt;code&gt;"Acme Co"&lt;/code&gt;, every single time, and you can point at the exact mapping that produced it. Same input, same output, forever — reviewable, diffable, testable, defensible.&lt;/p&gt;

&lt;p&gt;The tell is that even the AI-fill vendors know this. The same platforms shipping “fill any form with AI” also ship a deterministic, template-based mode — precisely because the instruction/LLM mode isn’t trusted for the forms where being wrong is expensive. When the stakes are real, everyone reaches for the deterministic path.&lt;/p&gt;
&lt;h2&gt;
  
  
  The write step the AI wave actually needs
&lt;/h2&gt;

&lt;p&gt;The clean architecture isn’t “AI does everything.” It’s a division of labor that matches each half to the right tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract with AI&lt;/strong&gt; — probabilistic, flexible, great for unseen and messy documents. This is where the model earns its keep.&lt;br&gt;
&lt;strong&gt;Fill deterministically&lt;/strong&gt; — a template plus a JSON of &lt;code&gt;field → value&lt;/code&gt;, applied exactly, with no model anywhere in the fill path. The output is auditable by construction.&lt;/p&gt;

&lt;p&gt;That second step is what PDFops is. You hand it an AcroForm template and a JSON object; it fills the fields exactly as specified, merges the result with any other PDFs you need, and returns the bytes — running on the V8 edge, no headless browser, no model in the loop. It’s the deliberately boring write hand that the clever AI read step can hand off to.&lt;/p&gt;
&lt;h2&gt;
  
  
  When you &lt;em&gt;should&lt;/em&gt; reach for AI fill
&lt;/h2&gt;

&lt;p&gt;To be fair to the other side: if you’re filling arbitrary, never-seen forms with no template — a long tail of one-off PDFs you can’t pre-map — a vision model is the only thing that works, and the AI-fill APIs are good at it. The deterministic path assumes you have, or can make, a template for the form.&lt;/p&gt;

&lt;p&gt;But most of what businesses actually fill is &lt;em&gt;not&lt;/em&gt; a long tail. It’s the same few dozen recurring, regulated, high-stakes forms — tax, insurance, HR, healthcare, real estate — over and over. For those, you already have the template, and the right write step is the deterministic one.&lt;/p&gt;
&lt;h2&gt;
  
  
  See it on your own PDF
&lt;/h2&gt;

&lt;p&gt;The fastest way to feel the difference: drop one of your form PDFs into the &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Form-Field Inspector&lt;/a&gt;. It lists every AcroForm field — name, type, options — and hands you the exact &lt;code&gt;fields&lt;/code&gt; JSON and API call to fill it. No signup, no model, no guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pdfops.dev/api/fill-form &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"pdf=@w9-template.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'fields={"name":"Acme Co","tin":"12-3456789","tax_classification":"C Corporation"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; filled.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same fields in, same PDF out, every run. If that’s the write step your pipeline needs, the &lt;a href="https://pdfops.dev/docs/fill-form" rel="noopener noreferrer"&gt;fill-form docs&lt;/a&gt; are the next stop — and the &lt;a href="https://pdfops.dev/#waitlist" rel="noopener noreferrer"&gt;waitlist&lt;/a&gt; is where to tell me about your volume and the forms you fill most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pdfops.dev/" rel="noopener noreferrer"&gt;← PDFops home&lt;/a&gt; · &lt;a href="https://pdfops.dev/blog" rel="noopener noreferrer"&gt;Blog&lt;/a&gt; · &lt;a href="https://pdfops.dev/tools/inspect" rel="noopener noreferrer"&gt;Field Inspector&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>api</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
