Skip to content
ParsedMail

Extract invoice data from email

To extract data from invoice emails, route supplier mail to one inbox, read both the body and the PDF attachment against a fixed schema (supplier, invoice number, dates, line items, tax, total), validate the numbers, block duplicates and only then create the bill in QuickBooks, Xero or your ERP.

Why invoice emails are a good candidate for automation

Accounts payable inboxes receive the same kind of document from many senders. Every supplier uses a different layout, but every invoice contains roughly the same facts: who is billing, which invoice it is, when it is due, what was bought and how much is owed. That combination, fixed meaning with variable layout, is exactly where manual retyping is slow and where rule-based tools need one template per supplier. An AI email parser reads the meaning instead of the position, so a new supplier does not need new setup.

A typical manual process looks like this: open the email, open the PDF, type supplier, invoice number, dates and total into the accounting system, sometimes the line items too, attach the PDF, and route it for approval. Automation replaces the typing, and if it is built carefully, also catches mistakes that people miss when they are tired.

Decide the fields before choosing a tool

Write the schema first. It forces the team to agree on what "done" means. A practical invoice schema:

FieldTypeNotes
supplier_namestringAs printed on the invoice
supplier_tax_idstringVAT or tax number if present, useful for matching
invoice_numberstringKeep as text, leading zeros matter
invoice_datedateYYYY-MM-DD
due_datedateNull if only payment terms are given
po_numberstringFor matching to purchase orders
line_itemstabledescription, quantity, unit_price (money), amount (money)
subtotalmoneyBefore tax
taxmoneyTotal tax
totalmoneyAmount due
currencyenumUSD, EUR, GBP and others you accept

In ParsedMail you can type this as a sentence ("supplier name, supplier tax id, invoice number, invoice date, due date, PO number, line items with description, quantity, unit price and amount, subtotal, tax, total") and adjust the generated types. Money values come back as {"amount": 1240.0, "currency": "EUR"}, so the currency never gets separated from the number. The invoice processing use case shows a full example.

Reading the body and the PDF together

Most invoices arrive as a short email with a PDF attached. The email body usually says "please find attached invoice 4471". The real data is in the PDF. Sometimes it is the other way around: a billing system sends an HTML email with the full invoice and a PDF copy.

A good extraction step reads both and merges the result, so you get one invoice object whichever part holds the values. Points to check in any tool:

  • PDFs with a text layer are read as text, which is fast and precise.
  • Scanned PDFs and photos need image reading. In ParsedMail that runs on paid plans, the sandbox reads text attachments of one page only.
  • Several attachments. Invoices sometimes come with terms and conditions or a delivery note. ParsedMail reads up to 8 files per email and skips small images such as logos and signature icons.
  • Page billing. One PDF page counts as one page on the plan, and roughly 3,000 characters of text or CSV count as one page.

Validation rules that catch real errors

Extraction can be wrong, and invoices themselves can be wrong. Run checks before anything reaches your ledger. These are cheap to write and catch most problems:

from decimal import Decimal

def check_invoice(inv, tolerance=Decimal("0.02")):
    problems = []
    lines = inv.get("line_items") or []
    amt = lambda m: Decimal(str(m["amount"])) if m else None

    line_sum = sum((amt(l["amount"]) for l in lines if l.get("amount")), Decimal("0"))
    subtotal, tax, total = amt(inv.get("subtotal")), amt(inv.get("tax")), amt(inv.get("total"))

    if subtotal is not None and lines and abs(line_sum - subtotal) > tolerance:
        problems.append(f"lines sum {line_sum} differs from subtotal {subtotal}")
    if None not in (subtotal, tax, total) and abs(subtotal + tax - total) > tolerance:
        problems.append("subtotal plus tax differs from total")
    for l in lines:
        q, p, a = l.get("quantity"), l.get("unit_price"), l.get("amount")
        if q and p and a and abs(Decimal(str(q)) * amt(p) - amt(a)) > tolerance:
            problems.append(f"line '{l.get('description')}' quantity times price differs from amount")
    if inv.get("due_date") and inv.get("invoice_date") and inv["due_date"] < inv["invoice_date"]:
        problems.append("due date before invoice date")
    if not inv.get("invoice_number"):
        problems.append("missing invoice number")
    return problems

Use Decimal, not floats, for money comparisons, and allow a small tolerance for rounding. Add your own rules too, for example a maximum amount per supplier, or a currency that must match the supplier record.

Combine these checks with the parser's own signals. In ParsedMail every field has a confidence score from 0 to 1, missing values are null, and a required field that is missing produces a warning. On Growth and Scale, emails below the inbox threshold (0.80 by default) wait in a human review queue where a person corrects the values next to the original email before they are delivered.

Stopping duplicate invoices

Paying the same invoice twice is one of the most common and most expensive AP errors. Duplicates come in two shapes:

  1. The same email twice. A forward plus an automatic rule, or a supplier resending. ParsedMail marks an email with the same Message-ID, or the same sender, subject and body, within 30 days in one inbox as duplicate. It is not parsed, not charged and not delivered.
  2. The same invoice in a different email. A reminder with the PDF attached again, or a corrected copy. Only your own data can catch this.

For the second case, keep a unique key in your system built from the supplier and the normalized invoice number:

def invoice_key(inv):
    supplier = (inv.get("supplier_tax_id") or inv.get("supplier_name") or "").strip().lower()
    number = "".join(ch for ch in (inv.get("invoice_number") or "") if ch.isalnum()).lower()
    return f"{supplier}:{number}"

If the key already exists, do not create a bill. Flag it, and let a person decide whether it is a reminder, a correction or a genuine new invoice with a reused number.

Posting bills to QuickBooks or Xero

Once an invoice passes the checks, the last step is creating a bill in the accounting system. There are two common ways:

  • No-code. A Zapier, Make or n8n scenario starts from the built-in webhook trigger, looks up the supplier as a vendor or contact, and creates a bill (QuickBooks) or a purchase invoice (Xero) with the line items. Each of these tools has a generic webhook trigger that receives the JSON.
  • Code. Your webhook receiver stores the event, a worker runs the validation from above, matches the supplier, and calls the accounting API.

Whichever way you choose, a few rules keep the books clean:

  • Create bills as drafts or awaiting approval, not as approved and scheduled for payment.
  • Attach the original PDF to the bill so an approver sees the source document.
  • Map supplier names to vendor records with a lookup table, not with fuzzy matching on every run.
  • Keep the ParsedMail email id on the bill (in a memo or reference field) so you can trace any bill back to its email.

Setting up the supplier inbox

Most companies already have an address such as invoices@ or ap@ that suppliers know. There is no need to change it. Instead, route a copy of that mail to the parsing inbox:

  • Gmail or Google Workspace. A filter on the AP address that forwards matching mail to the inbox address. Gmail sends a forwarding confirmation email first, which shows up in the ParsedMail log and is not parsed or counted.
  • Outlook or Microsoft 365. An inbox rule or a mail flow rule that forwards or BCCs a copy.
  • Your own mail server. An alias that delivers to both the team mailbox and the inbox address.

Plus tags help when one inbox serves several entities. Mail to [email protected] and [email protected] lands in the same inbox, and the to field of the result shows which address, and so which entity, it was meant for. Teams on the Scale plan can receive at their own domain through an MX record, for example [email protected] for the same inbox.

If the same inbox also receives statements, remittance advice and credit notes, classification on Growth and Scale lets one inbox hold several schemas. Each email is matched to the best-fitting schema, and the result says which one was chosen and how confident the match was.

A sensible rollout plan

Do not switch the whole AP inbox over in one day. A safer sequence:

  1. Add a BCC or forwarding rule so invoices go to the parsing inbox and to the usual mailbox. Nothing changes for the team yet.
  2. For two weeks, compare extracted values with what the team typed. Note which suppliers and fields produce low confidence.
  3. Restrict the inbox to known supplier addresses or domains with allowed senders. Mail from others is logged as blocked_sender and not processed.
  4. Turn on draft bill creation for high-confidence invoices only, and route the rest to review.
  5. Lower the share of manual review as the numbers support it.

Pricing for this kind of workload starts with the Starter plan at 1,000 emails and 300 pages a month. Invoice-heavy teams with multi-page PDFs usually look at Growth for the larger page allowance and the review queue. Details are on the pricing page.

Frequently asked questions

Can line items be extracted from a PDF invoice?

Yes. Define line items as a table field with typed columns such as description, quantity, unit price and amount. Each row comes back as an object in an array.

What if an invoice has no due date?

The field comes back as null. You can then calculate it from the supplier's payment terms in your own system rather than letting the parser guess.

How do I handle invoices in different currencies?

Use the money type. Each amount carries its own currency code, so a EUR invoice is never mistaken for USD when you sum or post it.

Are scanned invoices supported?

Yes on paid plans. Scanned PDFs and images are read as images. Check confidence closely on low-quality scans and route those to review.

Does this replace approval workflows?

No. Extraction replaces data entry. Approvals, payment runs and three-way matching stay in your accounting or AP system.

More from the blog

See your own email as JSON

Paste one into the demo, describe the fields and look at the result. No account needed.