---
title: "Bank statement to Excel with Python and Nutrient"
canonical_url: "https://www.nutrient.io/blog/bank-statement-to-excel/"
md_url: "https://www.nutrient.io/blog/bank-statement-to-excel.md"
last_updated: "2026-11-04T15:28:03.238Z"
description: "Extract bank statement data to Excel with Python and Nutrient. Add balance checks, citation details, and highlighted fields for review before using the results."
---

**TL;DR**

Extract bank statement data with Nutrient Data Extraction API, and write it to Excel with Python. The workbook includes balance checks and source citations, with yellow cells marking values that need attention. Check the results against the original statement before using them.

Missing transactions and misread amounts can leave a bank statement spreadsheet looking complete while its balances are wrong. This tutorial uses Nutrient Data Extraction API and `openpyxl` to build a workbook with transaction rows, balance checks, and source citations.

## Choose layout conversion or structured extraction

Convert a bank statement directly to a workbook with [Nutrient’s PDF-to-XLSX API](https://www.nutrient.io/api/pdf-to-xlsx-api/). Use these instructions in a multipart `/build` request:

```json

{
  "parts": [{ "file": "document" }],
  "output": { "type": "xlsx" }
}

```

For named fields such as `opening_balance` and a `transactions` array, use the Data Extraction API’s `/extract` endpoint with a JSON Schema. That’s the approach used below: Extract the fields, check their values, and arrange them in a workbook.

## What the script builds

The script creates four sheets: Summary, Transactions, Review, and Checks. Yellow cells mark citation or balance issues. It saves the full API response as a JSON file beside the workbook, so the source details stay available for lookup.

## Prerequisites

- Python 3.10 or later.

- A Nutrient Document Web Services (DWS) account and a key with Data Extraction access. [Create an account](https://dashboard.nutrient.io/sign_up/?product=data-extraction). Then follow the [API key setup guide](https://www.nutrient.io/guides/dws-data-extraction/getting-started.md#step-by-step-setup). The Data Extraction free plan includes 5,000 credits per month, with no credit card required.

- A statement for one account in one currency. The balance checks assume transactions are listed from oldest to newest, with credits positive and debits negative.

Use a synthetic or redacted statement while testing. The API processes the uploaded document; the workbook and JSON response can contain account details. Keep the API key, input files, and outputs out of version control. Review Nutrient’s [data handling guidance](https://www.nutrient.io/guides/dws-data-extraction/privacy.md) before uploading customer documents.

## Project setup

Create a project and install the dependencies. On systems that use `python3`, use it instead of `python` to create the virtual environment. These activation commands are for macOS or Linux; on Windows PowerShell, use `.venv\Scripts\Activate.ps1` instead of `source`:

```shell

mkdir bank-statement-extract
cd bank-statement-extract
python -m venv.venv
source.venv/bin/activate
python -m pip install requests python-dotenv openpyxl jsonschema

```

Create a `.env` file with the API key, and add `.env` to `.gitignore`:

```shell

NUTRIENT_API_KEY=your_data_extraction_api_key_here

```

Save each Python block below in the named file in this directory. Run the commands from this directory in the same terminal session.

## Step 1: Define the schema

Save this as `schema.py`. The descriptions tell the API what to extract. The script checks for required fields before writing the workbook and keeps the account number as a string to preserve leading zeros.

```python

# schema.py.

BANK_STATEMENT_SCHEMA = {
    "type": "object",
    "properties": {
        "bank_name": {
            "type": "string",
            "description": "Name of the financial institution that issued the statement",
        },
        "account_holder_name": {
            "type": "string",
            "description": "Name of the account holder the statement belongs to",
        },
        "account_number": {
            "type": "string",
            "description": "Account number or IBAN, however it's labeled on the statement",
        },
        "currency": {
            "type": "string",
            "description": "ISO 4217 currency code, e.g. USD or EUR",
        },
        "statement_start_date": {
            "type": "string",
            "format": "date",
            "description": "First date of the statement period",
        },
        "statement_end_date": {
            "type": "string",
            "format": "date",
            "description": "Last date of the statement period",
        },
        "opening_balance": {
            "type": "number",
            "description": "Account balance at the start of the statement period",
        },
        "closing_balance": {
            "type": "number",
            "description": "Account balance at the end of the statement period",
        },
        "transactions": {
            "type": "array",
            "description": "One entry per row in the transaction table, in statement order",
            "items": {
                "type": "object",
                "properties": {
                    "date": {"type": "string", "format": "date"},
                    "description": {
                        "type": "string",
                        "description": "Transaction description or memo as printed",
                    },
                    "amount": {
                        "type": "number",
                        "description": "Signed amount — negative for debits, positive for credits",
                    },
                    "running_balance": {
                        "type": "number",
                        "description": "Account balance after this transaction, if the statement shows one",
                    },
                },
                "required": ["date", "amount"],
            },
        },
    },
    "required": ["account_number", "currency", "opening_balance", "closing_balance", "transactions"],
}

```

The API supports a subset of JSON Schema, including objects, arrays, required fields, and date formats. It rejects unsupported keywords such as `$ref`, `anyOf`, and `minimum`. See the [schema guide](https://www.nutrient.io/guides/dws-data-extraction/extract/define-a-schema.md) for supported keywords and limits.

## Step 2: Call the extract endpoint

Save this as `extract.py`. The request enables citations and uses `understand`, the default parsing mode. It checks the HTTP status before reading a success response and handles error bodies that use either `errorMessage` or `message`.

```python

# extract.py.

import json
import os

import requests
from dotenv import load_dotenv

from schema import BANK_STATEMENT_SCHEMA

load_dotenv()
ENDPOINT = "https://api.nutrient.io/extraction/extract"

def extract_statement(pdf_path: str) -> dict:
    """Extract fields, retaining the full API response for review."""
    api_key = os.getenv("NUTRIENT_API_KEY")
    if not api_key:
        raise RuntimeError("Set NUTRIENT_API_KEY in your environment or.env file.")
    instructions = {
        "schema": BANK_STATEMENT_SCHEMA,
        "instructions": "Extract all transactions in statement order. Include fees and interest as separate rows. Use negative amounts for debits and positive amounts for credits. Do not invent missing transactions.",
        "parseConfig": {"mode": "understand"},
        "options": {"includeCitations": True},
    }
    try:
        with open(pdf_path, "rb") as source:
            response = requests.post(
                ENDPOINT,
                headers={"Authorization": f"Bearer {api_key}"},
                files={"file": source},
                data={"instructions": json.dumps(instructions)},
                timeout=(10, 330),
            )
    except requests.RequestException as exc:
        raise RuntimeError("Extraction request failed; check connectivity and retry deliberately.") from exc

    try:
        result = response.json()
    except ValueError as exc:
        raise RuntimeError(f"API returned HTTP {response.status_code} with a non-JSON body.") from exc

    if not response.ok:
        details = result if isinstance(result, dict) else {}
        message = details.get("errorMessage") or details.get("message") or "Request failed"
        raise RuntimeError(f"API HTTP {response.status_code}: {message}")
    if not isinstance(result, dict) or result.get("status")!= 200:
        raise RuntimeError("Unexpected extraction response; expected status 200.")
    if not isinstance(result.get("output"), dict) or not isinstance(result["output"].get("data"), dict):
        raise RuntimeError("Extraction response is missing output.data.")
    return result

```

The read timeout is slightly longer than the API’s five-minute processing limit. The server may still be processing after a timeout, so the script doesn’t retry automatically. See the [error handling guide](https://www.nutrient.io/guides/dws-data-extraction/errors.md) for status codes and troubleshooting.

Choose the parsing mode based on a representative sample of the statements:

| Mode         | When to evaluate it                                                       | Credits per page, including extraction |
| ------------ | ------------------------------------------------------------------------- | -------------------------------------- |
| `structure`  | Clean, predictable layouts; includes optical character recognition (OCR). | 7.5                                    |
| `understand` | Tables and more complex layouts; the default used here.                   | 15                                     |
| `agentic`    | Degraded scans or dense visual content that lower modes struggle with.    | 24                                     |

Test scanned statements in `structure` or `understand` before paying for `agentic`. Compare the results with the source documents. See [parse configuration](https://www.nutrient.io/guides/dws-data-extraction/extract/parse-configuration.md) for mode details and [pricing](https://www.nutrient.io/guides/dws-data-extraction/pricing.md) for all modes and costs.

## Step 3: Inspect the response

The API returns extracted values in `output.data` and available citation details in `output.metadata`. Metadata follows the same nested structure as the data, including transaction arrays. This excerpt comes from the live extraction of the fictional statement used below. It shows one transaction amount and selected citation fields:

```json

{
  "output": {
    "data": {
      "transactions": [
        {
          "amount": -42.18
        }
      ]
    },
    "metadata": {
      "transactions": [
        {
          "amount": {
            "match": "id_match",
            "citation": {
              "confidence": {
                "groundedness": 0.9997683167457581,
                "schemaValid": true
              },
              "regions": [
                {
                  "bbox": {
                    "height": 31.70635986328125,
                    "width": 77.5469970703125,
                    "x": 1091.9189453125,
                    "y": 902.34375
                  },
                  "pageIndex": 0,
                  "pageNumber": 1
                }
              ],
              "status": "cited"
            },
            "pageNumber": 1,
            "bbox": {
              "height": 31.70635986328125,
              "width": 77.5469970703125,
              "x": 1091.9189453125,
              "y": 902.34375
            }
          }
        }
      ]
    }
  }
}

```

This response includes `citation.confidence.groundedness` and no top-level `confidence` score. Keep those signals separate; the workbook records them under different columns when available. A missing top-level score doesn’t mean the source citation is missing. The checks below use match labels and source locations.

## Step 4: Check fields and balances

Save this as `review.py`. It flags missing citations, partial or approximate matches, and missing source locations. It doesn’t apply a shared threshold to different confidence signals or automatically approve any field.

```python

# review.py.

from decimal import Decimal

from jsonschema import FormatChecker, validate

from schema import BANK_STATEMENT_SCHEMA

def review_fields(data, metadata, path=""):
    """Yield each scalar value with its citation and any review reason."""
    if isinstance(data, dict):
        for key, value in data.items():
            citation = metadata.get(key, {}) if isinstance(metadata, dict) else {}
            yield from review_fields(value, citation, f"{path}.{key}" if path else key)
    elif isinstance(data, list):
        for index, value in enumerate(data):
            citation = metadata[index] if isinstance(metadata, list) and index < len(metadata) else {}
            yield from review_fields(value, citation, f"{path}[{index}]")
    else:
        citation = metadata if isinstance(metadata, dict) else {}
        match = citation.get("match")
        if not citation:
            reason = "Missing citation; check source"
        elif match not in ("id_match", "id_match_multiblock"):
            reason = f"Grounding needs review: {match or 'missing match'}"
        else:
            reason = ""
        if not reason and (citation.get("pageNumber") is None or not citation.get("bbox")):
            reason = "Missing source location; check source"
        yield {"field": path, "value": data, "reason": reason, "citation": citation}

def check_balances(data):
    """Validate schema and compare balances for a single-currency statement."""
    validate(data, BANK_STATEMENT_SCHEMA, format_checker=FormatChecker())

    def money(value):
        amount = Decimal(str(value))
        if not amount.is_finite():
            raise ValueError("Balances and amounts must be finite numbers.")
        return amount

    checks = []
    balance = money(data["opening_balance"])
    if not data["transactions"]:
        checks.append({"field": "transactions", "reason": "No transactions extracted; confirm against the statement"})
    for index, transaction in enumerate(data["transactions"]):
        balance += money(transaction["amount"])
        if transaction.get("running_balance") is not None and money(transaction["running_balance"])!= balance:
            checks.append({"field": f"transactions[{index}].running_balance", "reason": f"Balance mismatch: expected {balance}"})
    if balance!= money(data["closing_balance"]):
        checks.append({"field": "closing_balance", "reason": f"Opening balance plus transactions is {balance}; extracted closing balance is {data['closing_balance']}"})
    return checks

```

The balance checks use `Decimal` arithmetic. They compare each available running balance and the closing balance with the opening balance plus signed transactions. Schema validation catches missing required fields, wrong types, and invalid dates before workbook generation. If validation fails, inspect the retained JSON response.

Balance checks can catch missing or misread transactions, but errors that cancel each other out can still pass. Check empty transaction lists against the statement too. Statements with multiple accounts or currencies need a different schema and balance checks. See [citations and confidence](https://www.nutrient.io/guides/dws-data-extraction/extract/citations-and-confidence.md) for more on interpreting the metadata.

## Step 5: Write the workbook

Save this as `to_excel.py`. The script stores strings as text so Excel won’t interpret a description beginning with `=` as a formula. It writes dates as Excel date cells and keeps numeric fields as numbers.

```python

# to_excel.py.

import json
from datetime import date

from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill
from openpyxl.utils import get_column_letter

FLAG_FILL = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")

def put(sheet, row, column, value):
    """Write source text literally, even when it starts with an equals sign."""
    cell = sheet.cell(row=row, column=column, value=value)
    if isinstance(value, str):
        cell.data_type = "s"
    return cell

def write_statement_to_excel(data, reviews, checks, output_path):
    wb = Workbook()
    summary = wb.active
    summary.title = "Summary"
    flagged = {item["field"] for item in reviews if item["reason"]}
    flagged.update(check["field"] for check in checks)
    fields = [
        ("Bank", "bank_name"), ("Account holder", "account_holder_name"),
        ("Account number", "account_number"), ("Currency", "currency"),
        ("Statement start", "statement_start_date"), ("Statement end", "statement_end_date"),
        ("Opening balance", "opening_balance"), ("Closing balance", "closing_balance"),
    ]
    summary.append(["Field", "Value"])
    for row, (label, key) in enumerate(fields, start=2):
        put(summary, row, 1, label)
        value = data.get(key)
        if key.endswith("_date") and value is not None:
            value = date.fromisoformat(value)
        cell = put(summary, row, 2, value)
        if key in flagged:
            cell.fill = FLAG_FILL
    summary.append(["Review status", "Pending human review"])

    transactions = wb.create_sheet("Transactions")
    transactions.append(["Date", "Description", "Amount", "Running balance"])
    for index, transaction in enumerate(data["transactions"]):
        for column, key in enumerate(["date", "description", "amount", "running_balance"], start=1):
            value = transaction.get(key)
            if key == "date":
                value = date.fromisoformat(value)
            cell = put(transactions, index + 2, column, value)
            if f"transactions[{index}].{key}" in flagged:
                cell.fill = FLAG_FILL

    evidence = wb.create_sheet("Review")
    evidence.append(["Field", "Value", "Review reason", "Match", "Confidence (if returned)",
                     "Citation confidence metadata", "Page", "Bounding box"])
    for row, item in enumerate(reviews, start=2):
        citation = item["citation"]
        details = citation.get("citation")
        signals = details.get("confidence") if isinstance(details, dict) else None
        values = [item["field"], item["value"], item["reason"] or "Source matched; verify before use",
                  citation.get("match"), citation.get("confidence"),
                  json.dumps(signals) if signals is not None else None, citation.get("pageNumber"),
                  json.dumps(citation.get("bbox")) if citation.get("bbox") else None]
        for column, value in enumerate(values, start=1):
            cell = put(evidence, row, column, value)
            if item["reason"]:
                cell.fill = FLAG_FILL

    balances = wb.create_sheet("Checks")
    balances.append(["Field", "Result"])
    rows = checks or [{"field": "Balances", "reason": "Arithmetic matches; this does not prove extraction accuracy"}]
    for row, check in enumerate(rows, start=2):
        put(balances, row, 1, check["field"])
        put(balances, row, 2, check["reason"])
        if checks:
            balances.cell(row, 2).fill = FLAG_FILL

    for sheet in wb:
        sheet.freeze_panes = "A2"
        sheet.auto_filter.ref = sheet.dimensions
        for cell in sheet[1]:
            cell.font = Font(bold=True)
        for column in sheet.columns:
            width = min(65, max(14, max(len(str(cell.value or "")) for cell in column) + 2))
            sheet.column_dimensions[get_column_letter(column[0].column)].width = width
        for row in sheet:
            for cell in row:
                if isinstance(cell.value, date):
                    cell.number_format = "yyyy-mm-dd"
    wb.save(output_path)

```

Use the Review sheet to find available page numbers, bounding boxes, match labels, and confidence metadata. The top-level confidence score and nested citation signals occupy separate columns. The saved JSON contains additional details, including `source_bboxes` and page dimensions. Even when the balances match, the workbook stays marked “Pending human review.”

## Step 6: Run and inspect the result

Save this as `main.py`. It keeps the API response beside the workbook and refuses to overwrite existing output files:

```python

# main.py.

import json
import sys
from pathlib import Path

from jsonschema import ValidationError

from extract import extract_statement
from review import check_balances, review_fields
from to_excel import write_statement_to_excel

def process_statement(pdf_path: str, output_path: str) -> None:
    source = Path(pdf_path)
    target = Path(output_path)
    evidence_path = target.with_suffix(".json")
    if target.suffix.lower()!= ".xlsx":
        raise ValueError("Use an.xlsx output path.")
    if source.resolve() in (target.resolve(), evidence_path.resolve()):
        raise ValueError("Output files must not overwrite the source.")
    if target.exists() or evidence_path.exists():
        raise ValueError("Choose a new output name; the workbook or evidence file already exists.")

    result = extract_statement(str(source))
    evidence_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
    data = result["output"]["data"]
    checks = check_balances(data)
    reviews = list(review_fields(data, result["output"].get("metadata", {})))
    write_statement_to_excel(data, reviews, checks, target)
    print(f"Wrote {target}; status: pending human review")
    print(f"{sum(bool(item['reason']) for item in reviews)} citation review flag(s); {len(checks)} balance issue(s)")
    print(f"Full response and citations: {evidence_path}")

if __name__ == "__main__":
    try:
        if len(sys.argv)!= 3:
            raise ValueError("Usage: python main.py statement.pdf statement.xlsx")
        process_statement(sys.argv[1], sys.argv[2])
    except (OSError, RuntimeError, ValueError, ValidationError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)

```

For a first run, download the [synthetic bank statement](https://www.nutrient.io/downloads/bank-statement-example.pdf) and save it as `statement.pdf`. It contains fictional account details and two transactions. Run the script against that file:

```shell

python main.py statement.pdf statement.xlsx

```

Open the workbook and follow these steps:

1. Compare the account, currency, period, and balances on Summary with the source PDF.

2. Check transaction count and order, including rows continued across page breaks.

3. Inspect yellow cells, using Review and the saved JSON for source locations and review reasons.

4. Resolve entries on Checks. A matching balance still requires a check against the original statement.

5. Correct any errors and complete the review before using the data.

### Example workbook

Download the [recorded API response](https://www.nutrient.io/downloads/bank-statement-example-response.json) and the [example workbook](https://www.nutrient.io/downloads/bank-statement-example.xlsx) from the live test on 12 October 2026. The response omits the request identifier and remaining account credits; the extracted values and citation metadata are unchanged. All 16 fields matched the fictional statement, and both transactions reconciled. The checks produced no citation flags. This verifies one simple layout, not extraction accuracy across different banks.

The image below illustrates a separate review case: A manually constructed response gives the first transaction amount a fuzzy match. Its yellow cell shows how a review flag appears. This image doesn’t show the recorded live result.![Preview of generated Summary and Transactions sheets, with a yellow amount cell and its citation review reason](@/assets/images/blog/2026/bank-statement-to-excel/workbook-preview.png)

The downloadable workbook contains all four sheets. The fictional statement starts at 4,210.55, includes transactions of −42.18 and 2,100.00, and closes at 6,268.37.

For loan processing, [see how Nutrient extracts bank statements for lending workflows](https://www.nutrient.io/api/data-extraction-api/lending/).

### Additional layout and scan tests

We also ran the script in `understand` mode on five fictional statements on 12 October 2026. All 124 extracted field values matched the expected values, including account-number leading zeros, dates, transaction signs, and balances.

| Test statement                                                 | Extracted values | Balance checks                   |
| -------------------------------------------------------------- | ---------------- | -------------------------------- |
| USD with separate debit and credit columns                     | All matched      | No issues                        |
| EUR across two pages, with decimal commas and repeated headers | All matched      | No issues                        |
| GBP with newest transactions first                             | All matched      | Three running-balance mismatches |
| USD as a 150 dots per inch (DPI) grayscale scan                | All matched      | No issues                        |
| USD as a faint 100 DPI scan, rotated 90 degrees                | All matched      | No issues                        |

The GBP result exposes a limitation in this script: It checks running balances from the opening balance in row order, which assumes oldest-first transactions. The extracted values were correct, but the checks need adapting before you use newest-first statements.

All five responses had source locations and produced no citation flags. Inspection of the source crops found the expected values; some locations covered a whole text block.

These tests cover the five samples above, not real banks’ full range of layouts or scan quality. USD, EUR, and GBP were tested in separate statements; mixed currencies within one statement weren’t tested.

## Production considerations

- **Document coverage** — Test each bank’s layouts, date conventions, debit/credit signs, and page breaks. For reverse-ordered statements, adapt the schema instructions and balance checks before use.

- **Review policy** — Check why each field was flagged. Verify important fields against the source even when they aren’t highlighted.

- **Languages** — Set `parseConfig.options.language` for non-English statements. See [multilingual extraction](https://www.nutrient.io/guides/dws-data-extraction/parsing/multilingual-extraction.md).

- **Output handling** — Store the source and response together with appropriate access controls. Apply the retention policy to the workbook and JSON as well as the original statement.

## Try Nutrient with your bank statements

[Try Nutrient Data Extraction API](https://www.nutrient.io/api/data-extraction-api/) to extract account fields and transactions from bank statements. [Create an account](https://dashboard.nutrient.io/sign_up/?product=data-extraction), follow the [API key setup guide](https://www.nutrient.io/guides/dws-data-extraction/getting-started.md#step-by-step-setup), and run the script on the [sample PDF](https://www.nutrient.io/downloads/bank-statement-example.pdf). Compare the workbook with the source before adapting the schema and parsing mode for another bank’s layout.

## FAQ

#### Can Nutrient convert a bank statement PDF to Excel?

Yes. Use the PDF-to-XLSX API for direct conversion to a workbook. To extract specific account fields and transaction rows, use the Data Extraction API with a schema, and export the JSON to Excel.

#### Does the workbook contain validated financial data?

No. The script checks field types, citations, and balances, but those checks can miss extraction errors. Compare the workbook with the original statement before approving the data.

#### Does this work with scanned bank statements?

Yes. The `structure`, `understand`, and `agentic` modes include OCR. Start with `understand`, or test `structure` for simpler layouts. Try `agentic` if those modes struggle with degraded scans or complex content.

#### How much does bank statement extraction cost?

The extract endpoint charges the parsing cost plus 6 credits per page. The `understand` example costs 15 credits per page; `structure` costs 7.5 and `agentic` costs 24. Check [pricing](https://www.nutrient.io/guides/dws-data-extraction/pricing.md) for the current rates. New accounts receive 5,000 free credits per month.

#### Can I use the same schema for different banks?

The schema is reusable, but test the output for each bank. The balance checks here assume one account, one currency, and transactions ordered from oldest to newest. Adapt them for other statement structures.

## Related reading

- [Extract invoice data with a schema](https://www.nutrient.io/guides/dws-data-extraction/examples/extract-invoice-data-with-schema.md) — Apply the same endpoint to invoice fields and line items.

- [Citations and confidence](https://www.nutrient.io/guides/dws-data-extraction/extract/citations-and-confidence.md) — Interpret grounding and confidence signals.

- [Lending and underwriting data extraction](https://www.nutrient.io/api/data-extraction-api/lending/) — Explore bank statement extraction in lending workflows.

- [PDF-to-XLSX API](https://www.nutrient.io/api/pdf-to-xlsx-api/) — Convert a PDF directly to a spreadsheet.
---

## Related pages

- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [Adobe Pdf Embed Api Alternatives](/blog/adobe-pdf-embed-api-alternatives.md)
- [Adobe Pdf Services Api Alternatives](/blog/adobe-pdf-services-api-alternatives.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [`vector_store` holds your indexed documents (see the multimodal RAG post](/blog/agentic-rag.md)
- [Ai Assistant Agent Governance](/blog/ai-assistant-agent-governance.md)
- [How to build an AI agent for contract redlining against a compliance playbook](/blog/ai-contract-redlining-compliance-playbook.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Ai Document Workflows Ocr Compliance Heavy Teams](/blog/ai-document-workflows-ocr-compliance-heavy-teams.md)
- [Ai Legal Assistant Document Authoring](/blog/ai-legal-assistant-document-authoring.md)
- [Ai Schema Generator Document Extraction](/blog/ai-schema-generator-document-extraction.md)
- [Amazon Textract Alternatives](/blog/amazon-textract-alternatives.md)
- [Start (clears any prior buffer), navigate the document, then stop into a file.](/blog/android-faster-pdf-rendering.md)
- [Android Pdf Out Of Memory Handling](/blog/android-pdf-out-of-memory-handling.md)
- [Angular File Viewer Pdf Image Office Files](/blog/angular-file-viewer-pdf-image-office-files.md)
- [Approval Workflow Software](/blog/approval-workflow-software.md)
- [Approvals Matrix](/blog/approvals-matrix.md)
- [Apryse To Nutrient Migration](/blog/apryse-to-nutrient-migration.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Simple PII redaction.](/blog/automated-pii-removal.md)
- [Azure Document Intelligence Alternatives](/blog/azure-document-intelligence-alternatives.md)
- [Best Ai Document Workflow Platforms](/blog/best-ai-document-workflow-platforms.md)
- [Best Csharp Pdf Library](/blog/best-csharp-pdf-library.md)
- [Best Document Ai Platforms](/blog/best-document-ai-platforms.md)
- [Best Document Classification Platforms](/blog/best-document-classification-platforms.md)
- [Best document parser for RAG: LlamaParse vs. Unstructured vs. Reducto vs. Nutrient](/blog/best-document-parser-llamaparse-unstructured-reducto.md)
- [Best Document Parsing Apis](/blog/best-document-parsing-apis.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [Best Invoice Data Extraction Apis](/blog/best-invoice-data-extraction-apis.md)
- [Best Llm Document Understanding Platforms](/blog/best-llm-document-understanding-platforms.md)
- [Best Multilingual Ocr Software](/blog/best-multilingual-ocr-software.md)
- [Best Pdf Accessibility Apis](/blog/best-pdf-accessibility-apis.md)
- [Best Pdf Parsers For Rag](/blog/best-pdf-parsers-for-rag.md)
- [Best Pdf Processing Apis](/blog/best-pdf-processing-apis.md)
- [Best Pdf Viewer Apis](/blog/best-pdf-viewer-apis.md)
- [Best Salesforce Document Generation Apps](/blog/best-salesforce-document-generation-apps.md)
- [Best Secure Document Collaboration Platforms](/blog/best-secure-document-collaboration-platforms.md)
- [or](/blog/best-vue-pdf-viewers.md)
- [Bpm Guide](/blog/bpm-guide.md)
- [Bpm Tools](/blog/bpm-tools.md)
- [Build Vs Buy Document Extraction](/blog/build-vs-buy-document-extraction.md)
- [Business Automation](/blog/business-automation.md)
- [Capex Vs Opex](/blog/capex-vs-opex.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [1. Extract and chunk the PDF.](/blog/chat-with-pdf.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Construction Document Data Extraction](/blog/construction-document-data-extraction.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create And Edit Pdfs In Flutter](/blog/create-and-edit-pdfs-in-flutter.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [Creating And Filling Pdf Forms Programmatically In Javascript](/blog/creating-and-filling-pdf-forms-programmatically-in-javascript.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Nutrient document decision: Why we built a decision model for documents](/blog/decision-model-for-documents.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Docling Alternatives](/blog/docling-alternatives.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Authoring Audit Trail](/blog/document-authoring-audit-trail.md)
- [Document Authoring Spreadsheet Editor](/blog/document-authoring-spreadsheet-editor.md)
- [Document Extraction Confidence Scores](/blog/document-extraction-confidence-scores.md)
- [Document Extraction For Underwriting](/blog/document-extraction-for-underwriting.md)
- [Document Viewer](/blog/document-viewer.md)
- [Document Watermarking](/blog/document-watermarking.md)
- [Debian/Ubuntu.](/blog/easyocr-vs-tesseract-python.md)
- [Electronic signature API: How to sign PDFs programmatically](/blog/electronic-signature-api.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [Q3 sales](/blog/excel-to-markdown-for-llms.md)
- [Extend Alternatives](/blog/extend-alternatives.md)
- [Extract Patient Data On Premises](/blog/extract-patient-data-on-premises.md)
- [Extract Tables From Complex Pdfs](/blog/extract-tables-from-complex-pdfs.md)
- [app.py](/blog/extract-text-from-pdf-using-python.md)
- [Stricter thresholds for fields that move money. Calibrate on your own sample.](/blog/extraction-hallucination-detection.md)
- [Fillable Pdf](/blog/fillable-pdf.md)
- [Google Document Ai Alternatives](/blog/google-document-ai-alternatives.md)
- [How To Add Digital Signature To Pdf Using React](/blog/how-to-add-digital-signature-to-pdf-using-react.md)
- [How To Build A Dotnet Maui Pdf Viewer](/blog/how-to-build-a-dotnet-maui-pdf-viewer.md)
- [How To Build A Flutter Pdf Viewer](/blog/how-to-build-a-flutter-pdf-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer.md)
- [How To Build A Nextjs Pdf Viewer](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [Using Yarn](/blog/how-to-build-a-react-excel-viewer.md)
- [How To Build A React Native Pdf Viewer](/blog/how-to-build-a-react-native-pdf-viewer.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [Using Yarn](/blog/how-to-build-a-react-word-viewer.md)
- [How To Build A Reactjs File Viewer](/blog/how-to-build-a-reactjs-file-viewer.md)
- [How To Build A Reactjs Pdf Viewer With React Pdf](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer With Pdfjs](/blog/how-to-build-a-vuejs-pdf-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer](/blog/how-to-build-a-vuejs-pdf-viewer.md)
- [How To Build An Android Pdf Viewer](/blog/how-to-build-an-android-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Ng2 Pdf Viewer](/blog/how-to-build-an-angular-pdf-viewer-with-ng2-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Pdfjs](/blog/how-to-build-an-angular-pdf-viewer-with-pdfjs.md)
- [How To Convert Docx To Pdf Using Javascript](/blog/how-to-convert-docx-to-pdf-using-javascript.md)
- [How To Convert Docx To Pdf Using Python](/blog/how-to-convert-docx-to-pdf-using-python.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [How To Convert Html To Pdf Using React](/blog/how-to-convert-html-to-pdf-using-react.md)
- [How To Convert Html To Pdf Using Wkhtmltopdf And Csharp](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-csharp.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Convert Html To Pptx](/blog/how-to-convert-html-to-pptx.md)
- [How To Convert Pdf To Json](/blog/how-to-convert-pdf-to-json.md)
- [Quarterly report](/blog/how-to-convert-pdf-to-markdown-using-python.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [How To Convert Word To Pdf Using Javascript](/blog/how-to-convert-word-to-pdf-using-javascript.md)
- [or](/blog/how-to-create-a-react-js-signature-pad.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How to digitally sign PDFs in C# with Nutrient .NET SDK](/blog/how-to-digitally-sign-pdf-in-csharp.md)
- [How To Edit Pdfs Using Ios Pdf Library](/blog/how-to-edit-pdfs-using-ios-pdf-library.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Extract Tables From Pdf And Images](/blog/how-to-extract-tables-from-pdf-and-images.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [How to generate PDFs in C# with Nutrient .NET SDK](/blog/how-to-generate-pdf-in-csharp.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [How To Merge Pdfs Using Javascript](/blog/how-to-merge-pdfs-using-javascript.md)
- [How To Ocr Pdfs In Linux](/blog/how-to-ocr-pdfs-in-linux.md)
- [How To Print Pdf In Csharp](/blog/how-to-print-pdf-in-csharp.md)
- [How To Programmatically Create And Fill Pdf Form In Angular](/blog/how-to-programmatically-create-and-fill-pdf-form-in-angular.md)
- [C# barcode scanner: How to scan and read 1D barcodes and QR codes](/blog/how-to-scan-barcodes-in-csharp.md)
- [Open an image.](/blog/how-to-use-tesseract-ocr-in-python.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Html To Pdf In Javascript](/blog/html-to-pdf-in-javascript.md)
- [Ide Text Editors Cpp Large Scale](/blog/ide-text-editors-cpp-large-scale.md)
- [Numeric and identifier fields: exact single-block matches only.](/blog/insurance-document-extraction.md)
- [Intelligent Data Extraction](/blog/intelligent-data-extraction.md)
- [Invoice Approval Software](/blog/invoice-approval-software.md)
- [Javascript Document Editor](/blog/javascript-document-editor.md)
- [Javascript Pdf Editors](/blog/javascript-pdf-editors.md)
- [Javascript Pdf Libraries](/blog/javascript-pdf-libraries.md)
- [Keep Document Ai Private](/blog/keep-document-ai-private.md)
- [Kyc Onboarding Salesforce](/blog/kyc-onboarding-salesforce.md)
- [Landing Ai Alternatives](/blog/landing-ai-alternatives.md)
- [Langextract Vs Llamaindex Extraction Comparison](/blog/langextract-vs-llamaindex-extraction-comparison.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Uses OpenAI by default — set OPENAI_API_KEY.](/blog/llamaindex-vs-langchain-rag.md)
- [Llamaindex Vs Langchain Vs Haystack](/blog/llamaindex-vs-langchain-vs-haystack.md)
- [Llamaindex Workflows Vs Langgraph](/blog/llamaindex-workflows-vs-langgraph.md)
- [Llamaparse Alternatives](/blog/llamaparse-alternatives.md)
- [Llm Vs Document Parsing Api](/blog/llm-vs-document-parsing-api.md)
- [Low Code No Code Document Integrations](/blog/low-code-no-code-document-integrations.md)
- [One segment per document found, in page order:](/blog/mailroom-automation-api.md)
- [Material Requisition](/blog/material-requisition.md)
- [or](/blog/merge-pdfs.md)
- [Migrating Observability Hyperdx To Dash0](/blog/migrating-observability-hyperdx-to-dash0.md)
- [Swift Package Manager](/blog/mobile-pdf-sdk.md)
- [Numeric and identifier fields: exact single-block matches only.](/blog/mortgage-document-processing.md)
- [`elements` come from your document parser — each has a type and content.](/blog/multimodal-rag.md)
- [Multipage Extraction Page Boundaries](/blog/multipage-extraction-page-boundaries.md)
- [Nutrient Flutter 6 Bindings Api](/blog/nutrient-flutter-6-bindings-api.md)
- [Nutrient Flutter Bindings Architecture](/blog/nutrient-flutter-bindings-architecture.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Ocr Api Comparison](/blog/ocr-api-comparison.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Operations Management Software](/blog/operations-management-software.md)
- [Optical Character Recognition In Scanned Pdfs](/blog/optical-character-recognition-in-scanned-pdfs.md)
- [CPU](/blog/paddleocr-python-guide.md)
- [PDF accessibility for developers: Meeting WCAG 2.2, Section 508, and PDF/UA with an SDK](/blog/pdf-accessibility.md)
- [Extract data from PDF files: A developer guide to structured data from PDFs and scans](/blog/pdf-data-extraction-developer-guide.md)
- [Pdf Extraction Benchmark Opendataloader Bench](/blog/pdf-extraction-benchmark-opendataloader-bench.md)
- [Pdf Extraction Document Case Studies](/blog/pdf-extraction-document-case-studies.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdf Ua Validation](/blog/pdf-ua-validation.md)
- [Pdfjs Accessibility Structtree Printing](/blog/pdfjs-accessibility-structtree-printing.md)
- [Pdfjs Advanced Loading Streaming Workers](/blog/pdfjs-advanced-loading-streaming-workers.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.md)
- [Pdfjs Area Annotations Canvas Capture](/blog/pdfjs-area-annotations-canvas-capture.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Document Outline Bookmarks Metadata](/blog/pdfjs-document-outline-bookmarks-metadata.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [macOS](/blog/pdfjs-file-format-conversion-to-pdf.md)
- [macOS](/blog/pdfjs-generating-pdf-thumbnails-pdf2pic.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Native Annotation Layer Forms](/blog/pdfjs-native-annotation-layer-forms.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Pdf Page Manipulation Pdf Lib](/blog/pdfjs-pdf-page-manipulation-pdf-lib.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Server Side Text Extraction](/blog/pdfjs-server-side-text-extraction.md)
- [Pdfjs Sticky Note Annotations](/blog/pdfjs-sticky-note-annotations.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Pdfjs Thumbnail Sidebar](/blog/pdfjs-thumbnail-sidebar.md)
- [Pdfplumber Python Guide](/blog/pdfplumber-python-guide.md)
- [People Process Tools](/blog/people-process-tools.md)
- [Process Flows](/blog/process-flows.md)
- [React Docx Editor](/blog/react-docx-editor.md)
- [React Native Pdf Annotation](/blog/react-native-pdf-annotation.md)
- [React Pdf Annotation Layer Forms](/blog/react-pdf-annotation-layer-forms.md)
- [React Pdf Custom Rendering Hooks](/blog/react-pdf-custom-rendering-hooks.md)
- [Using Yarn](/blog/react-pdf-editor.md)
- [React Pdf Loading States Errors Passwords](/blog/react-pdf-loading-states-errors-passwords.md)
- [React Pdf Non Latin Fonts Special Pdfs](/blog/react-pdf-non-latin-fonts-special-pdfs.md)
- [React Pdf Outline Table Of Contents](/blog/react-pdf-outline-table-of-contents.md)
- [React Pdf Performance Optimization](/blog/react-pdf-performance-optimization.md)
- [React Pdf Setup Basic Rendering](/blog/react-pdf-setup-basic-rendering.md)
- [React Pdf Text Layer Custom Renderer](/blog/react-pdf-text-layer-custom-renderer.md)
- [React Pdf Thumbnails Page Navigation](/blog/react-pdf-thumbnails-page-navigation.md)
- [Reducto Alternatives](/blog/reducto-alternatives.md)
- [Requisition System](/blog/requisition-system.md)
- [labels.py](/blog/route-documents-automatically-classify-api.md)
- [or](/blog/sample-blog-updated.md)
- [Sdk Product Updates Q2 2026](/blog/sdk-product-updates-q2-2026.md)
- [System Of Record Vs Source Of Truth](/blog/system-of-record-vs-source-of-truth.md)
- [Add DWS MCP Server to your Claude Code project.](/blog/teaching-llms-to-read-pdfs.md)
- [Open an image file.](/blog/tesseract-python-guide.md)
- [The Six Best Pdf Generator Apis](/blog/the-six-best-pdf-generator-apis.md)
- [Define the HTML part of the document.](/blog/top-10-ways-to-generate-pdfs-in-python.md)
- [Top 5 Javascript Pdf Viewers](/blog/top-5-javascript-pdf-viewers.md)
- [Top Js Pdf Libraries](/blog/top-js-pdf-libraries.md)
- [Top Ten Ways To Convert Html To Pdf](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Unstructured Alternatives](/blog/unstructured-alternatives.md)
- [Using Javascript In Pdf Form Fields](/blog/using-javascript-in-pdf-form-fields.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Bpms](/blog/what-is-bpms.md)
- [What Is Business Logic](/blog/what-is-business-logic.md)
- [What Is Document Processing](/blog/what-is-document-processing.md)
- [What Is Intelligent Document Processing](/blog/what-is-intelligent-document-processing.md)
- [What Is Ocr Invoice Processing](/blog/what-is-ocr-invoice-processing.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)
- [Workflow Analysis](/blog/workflow-analysis.md)

