Vendors send invoices via email. Your accounting / ERP system wants structured data. The bridge between the two is usually a sad combination of a forwarded email, a person, and copy-paste.
This guide shows you how to replace that bridge with a webhook that POSTs the invoice details as JSON directly into your accounting API. The approach uses MailToAPI — sign up and you can follow along in real time.
Upfront: text-body invoices vs PDF invoices
MailToAPI v1 extracts from the email body, not from PDF attachments. If your invoices live entirely inside a PDF — like most enterprise vendor invoices — you need a PDF-parsing tool (Parseur, Klippa, or a custom OCR pipeline). If your invoice details are written in the email body (very common for SaaS subscription receipts, freelancer invoices, marketplace fees, payment processor receipts) — keep reading, this works.
Good fit for this guide:
- SaaS receipts (Stripe, Notion, Linear, Vercel, Railway, etc. — invoice data is in the email body)
- Freelancer / agency invoices (short body text with amount + due date)
- Marketplace fee summaries (Etsy, eBay, App Store) — fee breakdown in the email
- Payment processor weekly summaries
Bad fit (use a PDF-parsing tool):
- Vendor invoices that are just "Please find your invoice attached" + a PDF
- Itemized line-item tables that only exist in the PDF
Step 1 — Define the JSON shape you want
Think about what your accounting API actually consumes. A reasonable minimum for an invoice:
invoice_number string required
vendor string required
amount number required
currency string required
issue_date date optional
due_date date optional
description string optional
You can add more (line items, tax, billing period) but start lean. The LLM extracts more reliably when it has fewer, well-defined fields.
Step 2 — Create the inbox
In MailToAPI, create an inbox called e.g. "Vendor invoices" with the fields above. Set the destination URL to your accounting API endpoint (e.g. https://api.youraccounting.com/invoices) and any auth headers (Authorization: Bearer ...).
You'll get a forwarding address like [email protected].
Step 3 — Forward a real invoice
Forward a recent invoice email from one of your vendors to the address above. Within 10–30 seconds, you'll see a run in MailToAPI with:
- The extracted JSON
- The POST attempt to your destination URL
- Status:
delivered(your API returned 2xx) ordelivery_failed(something else)
If the extracted JSON looks wrong — e.g. the LLM misread a number or invented a date — you can edit the JSON manually and replay. Use this to refine your schema (add a description hint to fields the LLM gets wrong, drop fields that aren't reliably present).
Step 4 — Route real vendor invoices to that address
Two ways to do this:
Option A: Gmail filter (easiest)
In Gmail, create a filter like:
from:(stripe.com OR linear.app OR notion.so OR vercel.com)
subject:(invoice OR receipt)
Action: "Forward it to" → your MailToAPI address. See the Gmail forwarding guide for the verification flow.
Option B: Update the vendor's billing email
Most vendors let you change which email gets the invoice. Update the billing contact in each vendor's dashboard to [email protected] directly (or a forwarding alias on your domain that points there).
This is cleaner long-term — no Gmail filter to maintain — but takes a few minutes per vendor to update.
Step 5 — Receive the JSON on your accounting side
Your accounting API endpoint receives a JSON body like:
{
"invoice_number": "INV-3041",
"vendor": "Acme Vendor Co",
"amount": 1240.00,
"currency": "USD",
"issue_date": "2026-05-15",
"due_date": "2026-06-14",
"description": "Services rendered May 2026"
}
A typical Hono handler:
import { Hono } from 'hono';
const app = new Hono();
app.post('/invoices', async (c) => {
const inv = await c.req.json();
// persist, notify Slack, queue a payment, etc.
return c.json({ ok: true, id: inv.invoice_number });
});
export default app;
Step 6 — Quality control
For the first few weeks, monitor a few things:
- Validation failures — runs where the LLM couldn't extract a required field. Usually means you need to soften
requiredon fields not all vendors include, or refine the field description. - Numeric extraction — currencies and decimal separators (1.000,50 vs 1,000.50) can trip up extraction. If you see issues, add a description like "amount in decimal, US format (1234.56)".
- Dates — many vendors write "Due in 30 days" instead of a specific date. The LLM can't always resolve relative dates. Either accept that
due_datewill sometimes be missing or extract adue_in_daysinteger field instead.
What to do with the extracted data
- QuickBooks / Xero / Wave — most expose an Invoices API. Wrap your destination endpoint to translate the JSON shape and POST upstream.
- Postgres / your own DB — straightforward insert; add a
received_attimestamp. - Notion / Airtable — both have row-creation APIs. Map JSON keys to columns.
- Slack / email — flag invoices over a threshold for human review.
That's the loop. The hardest 20% is tuning your schema for your specific mix of vendors — the rest is plumbing.
