# Fillable PDF Form Data Extractor: Batch Forms to CSV & Excel (`naricky/pdf-form-data-extractor`) Actor

Merge hundreds of filled PDF forms (AcroForm and XFA) into one spreadsheet: one row per PDF, one column per field. Checkbox and radio values, stable column mapping, per-file status so one bad file never stops the batch, and a template-drift report that flags old form versions.

- **URL**: https://apify.com/naricky/pdf-form-data-extractor.md
- **Developed by:** [Ricky Lee](https://apify.com/naricky) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.75 / 1,000 pdf form processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Fillable PDF Form Data Extractor: Batch Forms to CSV & Excel

Merge hundreds of filled PDF forms (AcroForm and XFA) into one spreadsheet: one row per PDF, one column per field. Checkbox and radio values, stable column mapping, per-file status so one bad file never stops the batch, and a template-drift report that flags old form versions.

Built for HR, school and nonprofit offices, intake teams and program officers who send out one fillable PDF and get back dozens to thousands of completed copies. You don't need an Acrobat Pro seat, and you don't have to write a script.

### Features

- **One row per PDF, one column per field.** Field values are read from the PDF's form data, not from page text. No guessing and no OCR.
- **One bad file never stops the batch.** Each row has a `status` (`ok`, `no_form_fields_flattened`, `encrypted`, `corrupt`, `download_failed`) and an `error` message. Acrobat's "Merge Data Files into Spreadsheet" fails the whole export when one file is broken. This actor keeps going.
- **Template-drift report.** Forms are grouped by their field set. Any PDF whose fields differ from the majority is flagged `is_template_outlier`, and the report lists the missing, extra and likely renamed fields (usually a sign that someone filled in an old version of the form).
- **Checkboxes and radios done right.** Checkboxes output their export value (`Yes` / `Off`, or the custom on-value) or TRUE/FALSE. Radio groups output the selected option name, including `/Opt`-indexed groups. When a viewer saved the tick only in the widget appearance state and left the field value empty, the tick is still read.
- **XFA support.** The actor reads the XFA `datasets` packet for Adobe LiveCycle and dynamic forms whose AcroForm fields are empty or missing.
- **Stable layout for every intake cycle.** Map PDF field names to friendly column names, set the column order, and map old and new field names to one column. Schedule the actor against the same ZIP or key-value store prefix each cycle.
- **Blank-but-ok warning.** A form that has fields but no filled values gets a `warning` instead of passing silently as a blank row.
- **Spreadsheet-safe output.** The XLSX stores values as text, so ZIP codes, IDs and phone numbers keep their leading zeros. Values starting with `=`, `+`, `-` or `@` cannot run as formulas. The CSV is UTF-8 with a BOM so Excel opens accented and non-Latin text correctly.
- **Handles hierarchical names, duplicate names, password-protected files (open password or owner-only restrictions), and broken XFA packets.**

### Input

| Field | Description |
|---|---|
| `pdfUrls` | Direct HTTPS links to your filled PDFs |
| `zipUrl` | One ZIP of PDFs (subfolders are fine; macOS `__MACOSX` entries are skipped) |
| `keyValueStoreId` / `keyPrefix` | An Apify key-value store you uploaded PDFs to, optionally filtered by key prefix |
| `fieldMapping` | JSON object of PDF field name → column name |
| `keepUnmappedFields` | Keep fields that are not in the mapping (default on) |
| `columnOrder` | Columns to put first, in order |
| `checkboxMode` | `exportValue` (default) or `boolean` |
| `includeXfa` | Read XFA datasets (default on) |
| `escapeFormulasInCsv` | Neutralise formula-like values in the CSV (default on) |
| `pdfPassword` | Open password for protected PDFs (stored as a secret) |

Example:

```json
{
  "zipUrl": "https://example.com/intake-october.zip",
  "fieldMapping": {"applicant_name": "Name", "ApplicantName": "Name", "dob": "Date of birth"},
  "columnOrder": ["Name", "Date of birth"],
  "checkboxMode": "boolean"
}
```

### Output

**Dataset:** one row per PDF.

```json
{"source_file": "form-017.pdf", "status": "ok", "error": null, "warning": null, "form_source": "acroform",
 "field_count": 14, "filled_field_count": 12, "template_signature": "3f9c0a1b22de", "is_template_outlier": false,
 "Name": "Jane Doe", "Date of birth": "1990-04-02", "zip": "02134", "consent": true, "plan": "Gold"}
```

**Key-value store:**

- `merged.csv` and `merged.xlsx`: the merged table (frozen header row, text cells)
- `template_report.csv`: one row per template version, with file count, missing, extra and renamed fields, and file names
- `field_inventory.json`: every field with its type, how many files contain or fill it, and the checkbox, radio and choice options seen

### Pricing (pay per event)

| Event | Price |
|---|---|
| PDF form processed (status `ok`) | $0.00075 ($0.75 per 1,000 forms) |
| Flattened, encrypted, corrupt or undownloadable file | Free |

Example: a 500-form intake cycle costs $0.38.

### Privacy and limits

- The actor processes only the files you supply. It does no web scraping and sends nothing to third parties.
- Results are written to your run's own dataset and key-value store, under your Apify account's retention settings. Delete them when you're done if the forms hold personal data.
- There is no OCR. Scanned or flattened forms are reported as `no_form_fields_flattened` and are not charged.
- Limits: 50 MB per PDF and 500 MB per ZIP.

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct HTTPS links to your filled PDF forms (e.g. signed storage links or a public folder). Use this, a ZIP URL, a key-value store, or any mix.

## `zipUrl` (type: `string`):

Optional direct link to one ZIP file of PDFs. Every .pdf inside, including subfolders, is processed.

## `keyValueStoreId` (type: `string`):

Optional ID or name of an Apify key-value store where you uploaded the PDFs.

## `keyPrefix` (type: `string`):

Only process key-value store records whose key starts with this prefix, e.g. 'intake-2026-10-'. Empty means all records.

## `fieldMapping` (type: `object`):

Optional JSON object: PDF field name -> column name, e.g. {"topmostSubform\[0].Page1\[0].f1_01\[0]": "Name"}. Map old and new field names to the same column to merge template versions.

## `keepUnmappedFields` (type: `boolean`):

When a mapping is given, also output fields that are not in it (under their PDF field name). Turn off to get only the mapped columns.

## `columnOrder` (type: `array`):

Optional list of column names (after mapping) to put first, in this order. Other columns follow in first-seen order.

## `checkboxMode` (type: `string`):

How checked boxes appear. Radio buttons always output the selected option name.

## `includeXfa` (type: `boolean`):

Also read the XFA datasets packet (Adobe LiveCycle / dynamic forms) when the AcroForm fields are empty or missing.

## `escapeFormulasInCsv` (type: `boolean`):

Prefix values starting with = + - @ with an apostrophe in merged.csv so values typed by form fillers cannot run as spreadsheet formulas. The XLSX always stores them as text.

## `pdfPassword` (type: `string`):

Optional open password for password-protected PDFs. Files without a password still open normally.

## Actor input object example

```json
{
  "pdfUrls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f4506t.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fss4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8822.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f2848.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8821.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw7.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f4868.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f941.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1099nec.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8879.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4p.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sb.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sc.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sse.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8949.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040x.pdf"
    }
  ],
  "fieldMapping": {},
  "keepUnmappedFields": true,
  "checkboxMode": "exportValue",
  "includeXfa": true,
  "escapeFormulasInCsv": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `xlsx` (type: `string`):

No description

## `templateReport` (type: `string`):

No description

## `fieldInventory` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "pdfUrls": [
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f4506t.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fss4.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f8822.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f2848.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f8821.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw7.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f4868.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f941.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1099nec.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f8879.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw4p.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040sb.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040sc.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040sse.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f8949.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/f1040x.pdf"
        }
    ],
    "fieldMapping": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("naricky/pdf-form-data-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "pdfUrls": [
        { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f4506t.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/fss4.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f8822.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f2848.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f8821.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/fw7.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f4868.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f941.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1099nec.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f8879.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/fw4p.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040sb.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040sc.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040sse.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f8949.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/f1040x.pdf" },
    ],
    "fieldMapping": {},
}

# Run the Actor and wait for it to finish
run = client.actor("naricky/pdf-form-data-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "pdfUrls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f4506t.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fss4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8822.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f2848.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8821.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw7.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f4868.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f941.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1099nec.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8879.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4p.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sb.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sc.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040sse.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f8949.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/f1040x.pdf"
    }
  ],
  "fieldMapping": {}
}' |
apify call naricky/pdf-form-data-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,naricky/pdf-form-data-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/NAOgI4GG2d3ThdMcL/builds/7ZFom4A1yYeiv73OC/openapi.json
