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 device | No — stays in the browser | Yes — sent to the API to fill |
| Authoritative output | Client-produced (trust the client) | Server-produced (canonical) |
| Fill from server-only data | No — only what the page has | Yes |
| Form internals you manage | Typed accessors, flatten, appearances | None — handled server-side |
| Merge available | Yes — pdf-lib copyPages | Yes — /api/merge |
| Determinism | Deterministic (no AI) | Deterministic, audit-safe |
| Best for | Privacy-first, zero-upload, instant preview | Canonical 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.
← PDFops home · Playground · Python · Node