Overview
Invoices were arriving in Google Drive with no structure around them. No log, no categories, no month-end process. Finding out what had been spent — by whom, on what — meant opening files one by one. The answer was usually approximate and always late.
I built a system that monitors the Invoices folder daily, extracts key fields from PDFs and images via OCR, and logs every entry to a structured expense ledger. Anything it can't parse confidently gets flagged for human review. On the 27th of each month, a summary email goes out with the items that still need attention.
The goal: Replace manual invoice-sorting with an automated pipeline — and make month-end review something that happens, not something that gets skipped.
Architecture first
This tracker was built alongside an inventory system, and the question came up early: should expense tracking live in the same spreadsheet? The answer was no. Invoices carry financial data that not everyone who uses the inventory tracker needs to see. Separate files meant separate sharing permissions — a small architectural decision that kept access control simple without any extra configuration.
The data layer has three tabs: Expense Log (one row per invoice, with date, vendor, invoice date, category, amount, file name, and status), Expense Summary (monthly totals by vendor and category), and a Vendors reference tab that the OCR pipeline checks when trying to match an extracted name.
Two ways in
Expenses enter through two paths. Manual entry is always available — staff can log an invoice directly in the Expense Log with a date, vendor, category, amount, and file name. The second path is automatic.
Every morning at 8am, the Apps Script trigger scans the Invoices subfolder in Drive for new PDFs and images. For each file it hasn't processed before, it runs OCR to extract text, then attempts to pull three fields: vendor name, invoice date, and total amount. If the vendor matches an entry in the Vendors tab, the category populates automatically. The entry gets logged newest-first, and the file is tagged so it won't be processed again.
Any entry where a field couldn't be extracted — or where the vendor wasn't recognized — gets a status of Needs Review instead of Processed. That flag is the handoff back to a human.
The OCR integration
The trickiest part of the build was getting Drive API v2 OCR to work reliably. The pattern looks
simple: insert a file with ocr: true in the options, and Drive converts it to a Google
Doc you can read as text. In practice, it fails silently if the MIME type is wrong — and it's easy
to set the wrong one.
The resource object passed to Drive.Files.insert describes the source file, not
the output. Setting mimeType to "application/vnd.google-apps.document" —
which feels right, since that's what you want back — causes OCR to fail on valid PDFs without any
error. The fix is to use the blob's actual content type: whatever the file is, pass that.
Apps Script — OCR file insertion
// Wrong: describes the desired output, not the source
const resource = { title: "ocr_temp", mimeType: "application/vnd.google-apps.document" };
// Correct: use the blob's actual content type
const blob = file.getBlob();
const resource = { title: "ocr_temp_" + file.getId(), mimeType: blob.getContentType() };
const options = { ocr: true, ocrLanguage: "en" };
const tempDoc = Drive.Files.insert(resource, blob, options);
A related issue: the MimeType.GOOGLE_DOCS Apps Script constant — which you'd expect to
work — proved unreliable at runtime. The string literal
"application/vnd.google-apps.document" had to be used directly when checking file
types. Two small things. Both cost time.
Month-end, automated
A second trigger runs daily at 7am and checks whether the date is the 27th. If it is, it scans the Expense Log for any entries still marked Needs Review and sends a summary email with the file name, vendor (if detected), invoice date, and amount for each one.
If everything has been processed, the email still goes out — one line confirming there's nothing to do. The month-end review doesn't depend on anyone remembering to check.
Reflection
The vendor name extraction is the known open issue. The script reads the first five lines of the OCR output and takes the first non-empty one — which works when the invoice starts with the vendor's name, and doesn't when it starts with an account number, a greeting, or a logo caption. That's a parsing problem, not an OCR problem, and it's still partially unresolved. In the meantime, unmatched entries get flagged for review rather than logged incorrectly.
The Vendors tab was also built from scratch rather than linked to any existing list. That means it needs maintenance as the vendor roster changes. It's a small ongoing cost, but it's real — and it's the trade-off for having a file that only the right people can open.