Guide Automation

PDF invoices to Excel: free tools tested, paid APIs priced

On 10 sample invoices we made, two free table readers got 55 of 58 line items right. Paid APIs cost 1 to 10 cents a one-page invoice.

The short read

On 10 sample invoices we made, two free tools read 55 of 58 line items, all but the scan.

  • Amazon, Microsoft and Google sell invoice extraction for 1 to 10 cents a one-page invoice, and find the number and total without label rules.

What you can do with this

If your invoices are digital PDFs from a few suppliers, run the script in this guide and check that each invoice's lines add up to its total. Send every mismatch to a person.

An old ledger book lying open, its beige pages ruled into narrow empty columns for entries.
Photo: Dupedianer, CC BY-SA 4.0
In this article
  1. Our test: 10 invoices, four free tools
  2. Tables break in three predictable places
  3. 1. Pick the route by what your invoices look like
  4. 2. Check whether each PDF has a text layer
  5. 3. Try Excel’s PDF connector if you have Microsoft 365 on Windows
  6. 4. Run the script on a folder of invoices
  7. 5. Teach the script each supplier’s labels
  8. 6. Price the paid APIs for your volume
  9. When the free route stops paying
  10. Check the total on every invoice
  11. How we made this

If invoices arrive as PDFs and someone types them into Excel, free tools can do most of that typing. We made 10 sample invoices in nine layouts and ran four free tools on them. The two best, Camelot and tabula-py, read 55 of 58 line items exactly. All four read nothing from the scanned one; after OCR, the two we retried read it fully.

The paid route is cheap too. Amazon, Microsoft and Google sell invoice extraction for 1 to 10 cents a page or document, and those models find the invoice number and total for you. This guide shows where each free tool broke and why, gives you a script that writes a folder of invoices into a workbook, and prices the paid APIs for three monthly volumes. Prices and documentation were read on October 20, 2026.

Our test: 10 invoices, four free tools

We wrote a Python script that drew 10 invoices as PDFs with known values, so every extracted field could be scored against the truth. The vendors and amounts are made up. The layouts are the kinds a small office receives:

  • Ruled tables (lines around every cell), in seven of them.
  • Borderless tables with columns kept apart by spacing, in two.
  • A two-page invoice with 30 lines and the header repeated on page 2.
  • Descriptions that wrap onto a second line inside one cell.
  • European formats: 1.234,56 and 14.10.2026.
  • A credit line written as (120.00).
  • Labels set above their values in a small box, and an invoice number labelled “Reference” instead of “Invoice No”.
  • One scan: invoice 1 rendered as a 150 dpi image, with no text layer.

We scored two things. A line item counted only when both its description and its amount came out exactly right; there were 58. Then three header fields per invoice (number, date, total), 30 in all, read with one set of label rules we wrote before running anything.

Tool (version) Line items right (of 58) Header fields right (of 30) Where it failed
Camelot 2.0.0, lattice then stream 55 not tested (tables only) the scan
tabula-py 2.10.0 55 not tested (tables only) the scan
pdftotext 24.02 -layout plus our rules 53 24 the scan; split 2 wrapped lines
pdfplumber 0.11.10, default settings 45 24 the scan; found no table in the 2 borderless invoices
pdfplumber, text strategy 53 not rescored the scan; split 2 wrapped lines

After OCRmyPDF added a text layer to the scan, pdftotext and Camelot each read all 3 of its line items, and our label rules found all 3 header fields. The OCR was not perfect: two unit prices came back as 49.00. and 80.00:, with stray punctuation. The amounts we scored were clean, which is luck, not design.

Tables break in three predictable places

Every free tool here reads the text a PDF already contains and guesses which words belong in which cell. That guess is where they differ, and each failure in our table traces to one of three causes.

Lines or spacing. pdfplumber’s table finder defaults to lines for both directions, so it looks for drawn borders. On our two borderless invoices it found no table at all.

Switching both settings to text fixed those two. Camelot handles this by having two modes: lattice detects the grid from ruled lines, stream uses text alignment and whitespace. Our script tries lattice first and falls back to stream, which is why Camelot read both borderless invoices. tabula-py is a wrapper of tabula-java, which has the same two modes; with its default settings it read both borderless invoices.

Wrapped rows. When a description runs onto a second line, pdftotext prints two lines of text, and the second has no amount. Our line rules then saw the first half of the description as one item and dropped the rest. Camelot and tabula-py kept “Laptop repair, replaced keyboard and battery, tested” together in one cell. Microsoft documents the same problem for Excel’s PDF connector, where multi-line rows may need cleanup with Fill Down or Group By.

Labels. Header fields are not in a table, so a rule has to find them by their label. Our rules looked for “Invoice No”, “Invoice #” or “Invoice number” followed by a value on the same line. The invoice that put “Invoice number” above “CL-311” in a box beat them, and so did the one that said “Reference”. That is the gap the paid models close.

Amazon’s AnalyzeExpense maps “bill number, invoice number, receipt number” to one standard field, INVOICE_RECEIPT_ID, and returns a confidence score for every value.

One tabula-py trap is worth knowing before you start. By default it hands tables to pandas, which turned 49.00 into the number 49.0 in our first run. Our scorer expected text, so that run looked like a failure. Passing pandas_options={"dtype": str} keeps every cell exactly as printed, and the scores above use that setting.

1. Pick the route by what your invoices look like

The test points to a simple rule. Free tools are enough when your invoices are digital PDFs from a known set of suppliers whose layouts rarely change. You write label rules once per supplier and check the totals.

Pay for an extraction API when you get invoices from many suppliers, when many arrive as scans or phone photos, or when nobody on the team will maintain a script. Microsoft’s invoice model accepts phone-captured images, scanned documents and digital PDFs and supports invoices in 27 languages.

Do not use either route without a check step. Every tool we ran missed something, and the paid models return confidence scores precisely because they can be wrong too.

2. Check whether each PDF has a text layer

Run pdftotext -layout invoice.pdf - on one invoice. pdftotext is a command-line tool from the free Poppler PDF library. If it prints the invoice’s words, the PDF has a text layer and every free tool here can read it. If it prints nothing, the PDF is an image, and all four tools returned nothing on ours.

For image PDFs, add a text layer first: ocrmypdf scan.pdf scan.pdf writes the OCR text into the file. OCRmyPDF uses the open-source Tesseract engine and says plainly that its accuracy “may not match that of commercial OCR solutions”. Our clean, computer-made scan still picked up two stray characters, so expect more on a crooked phone photo.

Camelot’s own documentation says its built-in parsers “need a text-based PDF”, and pdfplumber’s says it works best on machine-generated rather than scanned PDFs.

3. Try Excel’s PDF connector if you have Microsoft 365 on Windows

Excel can read PDF tables itself through Power Query. Microsoft lists the PDF source for Excel for Microsoft 365 on Windows, where it requires .NET Framework 4.5 or later. The Mac list of data sources does not include PDF, and Excel for the web reads PDFs only from SharePoint.

We did not test it, because we had no Windows copy of Excel in our test environment. Here is what Microsoft’s connector documentation says:

  • Steps: pick the PDF source, choose the file, then in Navigator select a table and click Load, or Transform Data to clean it first.
  • Many files: to import a folder of PDFs at once, use the Folder or SharePoint Folder connector and combine the files.
  • Options in the Pdf.Tables function: StartPage and EndPage limit the pages read; MultiPageTables (on by default) joins similar tables on consecutive pages; EnforceBorderLines (off by default) makes drawn lines the cell boundaries.

The connector’s advantage is that the query refreshes. Drop next month’s invoices in the folder, click Refresh, and the table rebuilds. If our wrapped-row results are a guide, check any invoice with long descriptions by hand.

4. Run the script on a folder of invoices

The script below is the one we ran on our 10 test invoices. It reads every PDF in a folder and writes invoices.xlsx with two sheets: Invoices (number, date, total, the sum of the lines and a check) and Lines (one row per item, with the raw row kept for checking). It uses Camelot for line items and pdftotext plus label rules for the header.

It needs Python and Poppler’s pdftotext. We ran it on Linux with Python 3.11, where pdftotext came installed (the Ubuntu package is poppler-utils), and set it up with:

python3 -m venv invoices-env
invoices-env/bin/pip install camelot-py pandas openpyxl

Save this as invoices_to_excel.py:

"""Read every PDF invoice in a folder into invoices.xlsx: one sheet of
invoices, one of line items. Usage: python invoices_to_excel.py <folder>"""
import re, subprocess, sys
from datetime import datetime
from pathlib import Path
import camelot, pandas as pd

def money(s):
    s = re.sub(r"[$€£]|USD|EUR|GBP", "", str(s)).strip()
    neg = s.startswith("(") and s.endswith(")")
    s = s.strip("()")
    if re.fullmatch(r"-?\d{1,3}(\.\d{3})*,\d{2}", s): s = s.replace(".", "").replace(",", ".")
    elif re.fullmatch(r"-?\d{1,3}(,\d{3})*\.\d{2}", s): s = s.replace(",", "")
    else: return None
    return -float(s) if neg else float(s)

def date(s):  # 03/10/2026 is read as March 10 (US order)
    for fmt in ("%m/%d/%Y", "%d.%m.%Y", "%Y-%m-%d", "%B %d, %Y", "%b %d, %Y"):
        try: return datetime.strptime(s.strip(), fmt).date()
        except ValueError: pass

def header(text):
    no = re.search(r"invoice\s*(?:no\.?|number|#)\s*:?\s*(\S*\d\S*)", text, re.I)
    dt = re.search(r"(?:invoice date|date|issued)\s*:?\s*([A-Za-z]{3,9} \d{1,2}, \d{4}|[\d./-]{8,10})", text, re.I)
    tots = re.findall(r"(?:total(?: due)?|amount due)\s*:?\s*(?:USD|EUR)?\s*\$?\s*([\d.,()]+)", text, re.I)
    return {"number": no.group(1) if no else None, "date": date(dt.group(1)) if dt else None,
            "total": money(tots[-1]) if tots else None}

def lines(pdf):
    tables = camelot.read_pdf(str(pdf), pages="all", flavor="lattice")
    if tables.n == 0: tables = camelot.read_pdf(str(pdf), pages="all", flavor="stream")
    out = []
    for t in tables:
        for row in t.df.values.tolist():
            cells = [re.sub(r"\s+", " ", c).strip() for c in row if c.strip()]
            if len(cells) < 2 or money(cells[0]) is not None: continue
            if re.match(r"(sub)?total|amount due|.*tax", cells[0], re.I): continue
            if (amt := money(cells[-1])) is not None:
                out.append({"description": cells[0], "amount": amt, "row": " | ".join(cells)})
    return out

folder = Path(sys.argv[1]); pdfs = sorted(folder.glob("*.pdf"))
if not pdfs: sys.exit(f"No PDF files in {folder}")
invoices, items = [], []
for pdf in pdfs:
    rec = {"file": pdf.name}
    try:
        text = subprocess.run(["pdftotext", "-layout", str(pdf), "-"], capture_output=True,
                              text=True, check=True).stdout
        if not text.strip():
            rec["check"] = "no text layer: run ocrmypdf on it first"; invoices.append(rec); continue
        rec |= header(text)
        rows = lines(pdf); items += [{"file": pdf.name} | r for r in rows]
        rec["lines_sum"] = round(sum(r["amount"] for r in rows), 2)
        missing = [k for k in ("number", "date", "total") if rec[k] is None]
        rec["check"] = ("missing " + ", ".join(missing) if missing else
                        "ok" if rec["lines_sum"] == rec["total"] else "lines do not add up to total")
    except Exception as e:
        rec["check"] = f"error: {e}"
    invoices.append(rec)

out = folder / "invoices.xlsx"
with pd.ExcelWriter(out) as xl:
    pd.DataFrame(invoices).to_excel(xl, sheet_name="Invoices", index=False)
    pd.DataFrame(items).to_excel(xl, sheet_name="Lines", index=False)
print(pd.DataFrame(invoices)[["file", "number", "date", "total", "lines_sum", "check"]].to_string(index=False))
print(f"\n{len(items)} line items written to {out}")

Run it with invoices-env/bin/python invoices_to_excel.py invoices/. On our folder, with the scan already passed through OCRmyPDF, it printed:

     file      number       date   total  lines_sum                check
inv01.pdf    NWS-1042 2026-10-03  320.00     320.00                   ok
inv02.pdf   2026-0877 2026-10-07  308.91     308.91                   ok
inv03.pdf         NaN       None  740.00     740.00 missing number, date
inv04.pdf       50213 2026-10-14 2473.40    2473.40                   ok
inv05.pdf   KPW-77810 2026-10-12 1292.30    1292.30                   ok
inv06.pdf     FP-5531 2026-10-09  274.00     274.00                   ok
inv07.pdf         NaN 2026-10-15  372.60     345.00       missing number
inv08.pdf DGD/26/4410 2026-10-16  591.50     591.50                   ok
inv09.pdf    NWS-1042 2026-10-03  320.00     320.00                   ok
inv10.pdf    OSL-9001 2026-10-01 1080.00    1080.00                   ok

58 line items written to invoices/invoices.xlsx

Eight invoices came out fully right, and the two it could not read say so instead of filling in a wrong value. Our first version did not do that. It took the word “Invoice” from the boxed header as the invoice number, so we made the rule require a digit. Run on the scan before OCR, the check column reads no text layer: run ocrmypdf on it first.

The script rewrites invoices.xlsx on every run, so run it on a copy of the folder, and close the workbook in Excel first. A PDF that crashes a tool gets an error: row instead of stopping the run. An empty folder stops with a message.

5. Teach the script each supplier’s labels

The missing rows tell you which rule to extend. For invoice 7, adding reference to the number pattern, as (?:invoice\s*(?:no\.?|number|#)|reference), picked up “CC-2026-118” when we tried it. Its total check would still fail, because that invoice adds 8% sales tax to the lines. Compare the lines with the subtotal on invoices that list tax, or add the tax line to the sum.

Three settings deserve a look before you trust the output:

  • Date order. The script reads 03/10/2026 as March 10. If your suppliers write the day first, move %d/%m/%Y ahead of %m/%d/%Y in date(), or split suppliers into folders by country.
  • Decimal commas. money() reads both 1,234.56 and 1.234,56, but only with two decimals. An amount printed without cents, such as 1,200, is not read as money.
  • Borderless tables. If a supplier’s table has no lines and Camelot’s stream mode merges columns, try tabula-py with pandas_options={"dtype": str}, which read all of ours.

6. Price the paid APIs for your volume

If the check column fills with missing, the paid models are worth pricing. All three return the invoice number, the total and the line items as named fields (Amazon, Microsoft, Google), with no label rules to write. These are list prices as read on October 20, 2026; the monthly costs assume one-page invoices.

Service Price 100 invoices a month 500 2,000 Free allowance
Amazon Textract AnalyzeExpense $0.01 a page (US West, Oregon) $1 $5 $20 100 pages a month for 3 months, new customers
Azure Document Intelligence, prebuilt invoice (S0) $10 per 1,000 pages (East US) $1 $5 $20 Free tier: 500 pages a month, only the first 2 pages of each file
Google Document AI Invoice parser $0.10 per document of up to 10 pages $10 $50 $200 None listed for the parser; Google Cloud Free Program credit of $300

The prices come from Amazon’s Textract pricing page, whose own example prices AnalyzeExpense at $0.01 a page in US West (Oregon) and $0.008 after one million pages; from Microsoft’s Azure Retail Prices API for East US, which lists prebuilt pages at $10 per 1,000; and from Google’s Document AI pricing page, where one “count” equals up to 10 pages. Azure’s free tier also caps files at 4 MB and one request per second, and its 500-page allowance is on the pricing page.

The per-document price makes Google the costliest for one-page invoices and the closest for long ones. A 10-page invoice costs $0.10 at Google and $0.10 at the other two.

What the table leaves out matters as much. Each service needs a cloud account with billing set up, and each means sending your suppliers’ invoices to that cloud. We did not run any of the three, because that needs a paid account. Cloudflare’s preview invoice model is cheaper still on its free tier, but it does not accept PDFs, only images.

When the free route stops paying

Each new supplier layout costs a rule, and each rule needs a test. Our payback formula counts that upkeep. At 500 one-page invoices a month, Amazon or Azure would cost $5 a month in API fees. Put the hours you spend writing and fixing label rules against that figure.

Our view: a script pays only while the layouts stay few and fixed.

If your invoices arrive by email, our guide to saving Outlook attachments automatically gets the PDFs into a folder for the script. For the fields in the email body itself, see email data into Excel. More guides like these are on the automation topic page.

Check the total on every invoice

Whichever route you pick, keep the check column. Compare the sum of the lines with the printed total, or with the subtotal when tax is listed, and send every mismatch to a person. In our test, every invoice the script could not read flagged itself that way, and every invoice marked ok was right.

How we made this

On October 20, 2026, we generated 10 sample invoices with the ReportLab library, including one rendered as an image to stand in for a scan, and scored Camelot, tabula-py, pdfplumber and pdftotext against their known values; the vendors and amounts are made up. We ran the script in this guide on the same files, on Linux with Python 3.11, and read the vendors’ pricing pages, the Azure price API and each tool’s documentation that day. We did not test Excel’s PDF connector or any paid API, and our invoices are cleaner than real scans, so treat the scores as a best case.

Sources