Guide

Fill a PDF in JavaScript

JavaScript gives you a choice no other language quite does: you can fill a PDF entirely in the browser, so the file never leaves the user’s machine, or you can fill it server-side when the output must be authoritative. This page shows both with runnable code, and is honest about when each is the right call.

In the browser, client-side (the file never uploads)

With pdf-lib 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:

import { PDFDocument } from "pdf-lib";

const input = document.querySelector("#pdf");      // <input type="file">
input.addEventListener("change", async () => {
  const bytes = await input.files[0].arrayBuffer();
  const doc = await PDFDocument.load(bytes);
  const form = doc.getForm();
  form.getTextField("customer_name").setText("Acme Co");
  form.getTextField("invoice_total").setText("$1,250.00");
  // form.flatten();  // optional: bake values in

  const out = await doc.save();
  const url = URL.createObjectURL(new Blob([out], { type: "application/pdf" }));
  Object.assign(document.createElement("a"), { href: url, download: "filled.pdf" }).click();
});

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 and merge, running live in the PDFops playground, which does all its work client-side.

Server-side, via one fetch

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 fetch works from a Worker, an edge function, or a Node backend:

const form = new FormData();
form.append("pdf", fileBlob, "template.pdf");      // a Blob/File of the template
form.append("fields", JSON.stringify({
  customer_name: "Acme Co",
  invoice_total: "$1,250.00",
}));

const resp = await fetch("https://pdfops.dev/api/fill-form", {
  method: "POST",
  body: form,
});
const filled = await resp.arrayBuffer();           // the filled PDF bytes

In production, call this from your own backend or edge function 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 Form-Field Inspector to get the exact field names for your fields object.

Browser or server: how to choose

Client-side (pdf-lib in browser)Server-side (PDFops API)
File leaves the deviceNo — stays in the browserYes — sent to the API to fill
Authoritative outputClient-produced (trust the client)Server-produced (canonical)
Fill from server-only dataNo — only what the page hasYes
Form internals you manageTyped accessors, flatten, appearancesNone — handled server-side
Merge availableYes — pdf-lib copyPagesYes — /api/merge
DeterminismDeterministic (no AI)Deterministic, audit-safe
Best forPrivacy-first, zero-upload, instant previewCanonical records, server data, store/sign/merge

The honest summary: if privacy or zero-upload is the point, fill client-side with pdf-lib — 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.

Merging PDFs in JavaScript too

Server-side, merge is the same one-call primitive:

const form = new FormData();
for (const blob of pdfBlobs) form.append("pdfs", blob);
const merged = await (await fetch("https://pdfops.dev/api/merge", {
  method: "POST", body: form,
})).arrayBuffer();

Client-side, pdf-lib merges with copyPages into a fresh document. Either way the fill-then-merge flow stays deterministic end to end. Endpoint details: fill-form docs, merge docs.

Frequently asked

How do I fill a PDF form in the browser with JavaScript?

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 playground does exactly this, live.

Should I fill in the browser or on the server?

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.

Can I call the PDFops API from browser JavaScript?

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.

Is pdf-lib enough to fill a PDF in JavaScript?

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.

Does filling change the bytes unpredictably?

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 this essay.

Try it in 30 seconds

Fill and merge a real PDF client-side, right now, in the playground — no signup. On another stack? See fill a PDF in Python and fill a PDF in Node.

If the deterministic fill + merge primitive fits your usage, join the waitlist and tell me the forms you fill most — that signal is what the pricing tiers and the in-function library get built around.