PDFOven is live on DevHunt this week — check it out →
← All guides

How to fill PDF forms programmatically (2026)

Government forms, onboarding packets, insurance claims: plenty of workflows end with "and then fill in this PDF". If the PDF has real form fields, you can fill it in code in a few lines. The work is in finding out what the fields are called, what values they accept, and making the result look right in every viewer. This guide covers that with pdf-lib and pypdf, then the two-request PDFOven version.

AcroForm basics

A fillable PDF (an AcroForm) has a list of named fields. Each field has a type, such as text, checkbox, radio group, dropdown or option list, and one or more widgets: the boxes you see on the page. A field's value is stored separately from its appearance stream, which is a small drawing of how the filled box should look. Viewers show the appearance stream. That split is the root of most "I filled it but it's blank" bugs.

If your PDF has boxes but no fields (a scan, or a form that was flattened), there's nothing to fill. You would have to draw text at coordinates instead.

Step 1: list the fields

You can't fill fields until you know their exact names.

pdf-lib (Node.js)

import { readFile } from "node:fs/promises";
import {
  PDFDocument, PDFTextField, PDFCheckBox, PDFRadioGroup, PDFDropdown, PDFOptionList,
} from "pdf-lib";

const pdf = await PDFDocument.load(await readFile("application.pdf"));
for (const field of pdf.getForm().getFields()) {
  const name = field.getName();
  if (field instanceof PDFTextField) console.log("text", name, field.getText() ?? "");
  else if (field instanceof PDFCheckBox) console.log("checkbox", name, field.isChecked());
  else if (field instanceof PDFRadioGroup || field instanceof PDFDropdown || field instanceof PDFOptionList)
    console.log("choice", name, field.getOptions());
  else console.log("other", name);
}

pypdf (Python)

from pypdf import PdfReader

reader = PdfReader("application.pdf")
for name, field in (reader.get_fields() or {}).items():
    print(name, field.get("/FT"), field.get("/V"), field.get("/_States_", ""))

/FT is the field type (/Tx text, /Btn checkbox or radio, /Ch choice). For radio buttons, and for many checkboxes, pypdf adds /_States_ with the values the field accepts, which you need in the next step. When it's missing, the on-value is the key other than /Off in the widget's /AP /N dictionary.

Step 2: fill and flatten

pdf-lib

import { writeFile } from "node:fs/promises";

const form = pdf.getForm();
form.getTextField("applicant_name").setText("Ada Lovelace");
form.getCheckBox("agree_terms").check();
form.getRadioGroup("plan").select("Annual");
form.getDropdown("state").select("OR");

form.flatten();
await writeFile("filled.pdf", await pdf.save());

flatten() updates the appearance streams, draws them into the page content and removes the fields, so the result is read-only and looks the same everywhere. Leave it out if the recipient should still be able to edit the form.

pypdf

from pypdf import PdfWriter

writer = PdfWriter(clone_from="application.pdf")
writer.update_page_form_field_values(
    None,
    {"applicant_name": "Ada Lovelace", "agree_terms": "/Yes", "state": "OR"},
    auto_regenerate=False,
)
writer.write("filled.pdf")

Passing None as the page fills fields on every page (pypdf 4.3 and later). Recent releases (5.9 and later) also accept flatten=True in the same call.

The gotchas

Field names come from the authoring tool. You'll see names like topmostSubform[0].Page1[0].f1_01[0] or Text7 rather than name. Those are hierarchical names created by the designer, and they're what you must use. Build a mapping from your data to field names once per form, and keep it in version control next to a copy of the form, since a new revision of the form can rename everything.

Checkbox on-values aren't always "Yes". A checkbox is checked by setting it to the name of its "on" appearance, which the form designer chose: /Yes, /On, /1, /Checked and others are all common. pdf-lib's check() looks it up for you; with pypdf, find it as described in step 1. Radio groups work the same way, with one on-value per option.

Appearance streams and NeedAppearances. If you set a value without regenerating its appearance, some viewers show the new value and others show the old empty box. Setting the form's NeedAppearances flag asks viewers to redraw fields themselves, and pypdf's set_need_appearances_writer(True) does that, but not every viewer honours it, and mobile previewers and print pipelines often don't. Regenerating appearances, or flattening, is the reliable route.

XFA forms aren't AcroForms. Some forms, especially older government and Adobe LiveCycle forms, are XFA: an XML form description that only Adobe's own software fully renders. Many have an AcroForm copy as a fallback, which is what pdf-lib and pypdf fill. pdf-lib removes the XFA data when you call getForm() (and logs a warning) so viewers use the AcroForm. A purely dynamic XFA form with no AcroForm can't be filled with either library.

Non-Latin text. Appearance streams have to draw your text with a font. Many forms specify Helvetica, which only covers Latin characters, so filling a field with Greek, Cyrillic or CJK text can fail to draw or show empty boxes. In pdf-lib, embed a font that has the glyphs (with @pdf-lib/fontkit) and call form.updateFieldAppearances(font) before saving.

The PDFOven way: two calls

PDFOven splits the job into the same two steps.

1. List the fields (free)

curl https://pdfoven.com/v1/pdf/form/fields \
  -H "Authorization: Bearer $PDFOVEN_KEY" \
  -F file=@application.pdf
{
  "data": [
    {
      "name": "applicant_name", "type": "text", "required": true, "read_only": false,
      "pages": [1], "value": null, "multiline": false, "max_length": 40
    },
    {
      "name": "agree_terms", "type": "checkbox", "required": false, "read_only": false,
      "pages": [2], "value": false
    },
    {
      "name": "state", "type": "dropdown", "required": false, "read_only": false,
      "pages": [1], "value": null, "options": ["CA", "OR", "WA"], "editable": false, "multi_select": false
    }
  ],
  "page_count": 2
}

Each field has its name, type (text, checkbox, radio, dropdown, option_list and so on), whether it's required or read_only, the pages it appears on, its current value, options for choice fields and max_length for text. Listing fields doesn't use a render.

2. Fill, and optionally flatten

curl https://pdfoven.com/v1/pdf/form/fill \
  -H "Authorization: Bearer $PDFOVEN_KEY" \
  -F file=@application.pdf \
  -F 'fields={"applicant_name":"Ada Lovelace","agree_terms":true,"state":"or"}' \
  -F flatten=true \
  --output filled.pdf

The rules are deliberately forgiving:

  • Text fields take strings or numbers.
  • Checkboxes take true or false; you never need to know the on-value.
  • Radio and dropdown values must match one of the field's options, ignoring case, so "or" selects OR.
  • flatten: true makes the result read-only.

A misspelled field name is an error that suggests the closest match, for example No field named "aplicant_name" (did you mean "applicant_name"?). Send "strict": false to skip unknown names instead.

From Node.js, by URL

const res = await fetch("https://pdfoven.com/v1/pdf/form/fill", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PDFOVEN_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file: { url: "https://example.com/forms/application.pdf" },
    fields: { applicant_name: "Ada Lovelace", agree_terms: true, state: "OR" },
    flatten: true,
  }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}

Text that the form's font can't draw, such as CJK characters, can still be filled: PDFOven leaves those fields for the viewer to draw. Such a form can't be flattened, though, and asking for flatten returns unsupported_text. XFA-only forms have no AcroForm fields to fill and return no_form.

Filling counts as one render. Files can be up to 50 MB, and password-protected forms take password. AI agents get list_pdf_form_fields and fill_pdf_form through the MCP server, which works well for "fill this form from that email" tasks. For filling a single form by hand, the free PDF form filler runs in the browser.

When to use which

  • pdf-lib when you fill a handful of known forms from JavaScript and want full control, including custom fonts for non-Latin text.
  • pypdf for Python pipelines, keeping in mind that you handle on-values and appearances yourself.
  • PDFOven when you fill many different forms, want checkbox and option matching handled for you, or want typo-tolerant errors when a form changes. Listing fields is free, and filling uses one of your 100 free monthly renders; paid plans are $9, $39 and $99 for 2,000, 15,000 and 60,000.

Skip the browser plumbing

PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.

Get a free API key