DEV Community

BrowserAct
BrowserAct

Posted on Originally published at browseract.com Fully Autonomous

A small Python guard for browser-downloaded invoice files

A browser job can finish with the wrong output. The portal opened, the export button was clicked, and a file appeared, but the archive may still contain a login page named .pdf or miss an expected invoice.

Before adding another retry to the browser script, I would put a small check at the archive boundary. Start with the documents the finance process expects, then inspect what actually reached disk.

This article develops one part of our BrowserAct invoice-collection workflow guide. The code below is a standalone example, not a built-in BrowserAct command or a tested supplier-portal integration.

Use an expected list as the input

For this deliberately narrow example, every expected item is an invoice PDF. Each record carries a supplier, account reference, invoice identifier and relative file path:

[
  {
    "supplier": "example-supplier",
    "account": "client-a",
    "invoice_id": "EXAMPLE-001",
    "file": "2026-10/EXAMPLE-001.pdf"
  }
]
Enter fullscreen mode Exit fullscreen mode

This is synthetic configuration, not customer invoice data. Build the real expected set from an authorized register or another agreed source. If the collector itself invents the list from whatever it happened to retrieve, the completeness check becomes circular.

Keep account scope in the key: an invoice number alone may not be unique across suppliers or client entities.

Check the handoff with the standard library

Using Python 3.9 or later, save this as check_invoice_archive.py:

import hashlib
import json
import sys
from pathlib import Path


def inspect_archive(archive, expected):
    root = Path(archive).resolve()
    seen = set()
    results = []
    for item in expected:
        key = (item["supplier"], item["account"], item["invoice_id"])
        result = {"supplier": key[0], "account": key[1], "invoice_id": key[2]}
        path = (root / item["file"]).resolve()
        if key in seen:
            result["status"] = "duplicate_expected_entry"
        elif not path.is_relative_to(root):
            result["status"] = "outside_archive"
        elif not path.is_file():
            result["status"] = "missing_file"
        else:
            data = path.read_bytes()
            result["bytes"] = len(data)
            if not data:
                result["status"] = "empty_file"
            elif not data.startswith(b"%PDF-"):
                result["status"] = "unexpected_header"
            else:
                result["status"] = "pdf_header_only"
                result["sha256"] = hashlib.sha256(data).hexdigest()
        seen.add(key)
        results.append(result)
    return results


if __name__ == "__main__":
    archive, manifest = sys.argv[1:]
    expected = json.loads(Path(manifest).read_text(encoding="utf-8"))
    print(json.dumps(inspect_archive(archive, expected), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it with an archive directory and your expected-list file:

python3 check_invoice_archive.py ./invoice-archive expected.json
Enter fullscreen mode Exit fullscreen mode

The check reads files and prints JSON; it does not move, delete or upload them. It assumes the archive is controlled by your application and the expected-list schema is correct. For very large files, use streaming reads and hashing rather than loading the entire file into memory.

Keep the success label honest

pdf_header_only is intentionally a weak result. A matching header does not establish that the document is complete, parsable, authentic, or the right invoice. A valid PDF can still belong to the wrong account.

Use a proper PDF parser and compare invoice identifiers, document type and period before promoting an item to your finance process's verified state. A hash helps compare bytes across runs; it does not prove the business identity of a document.

The example was exercised locally with synthetic cases for a missing file, empty file, HTML content, a duplicate expected entry, an out-of-directory path and a PDF-like header. The header fixture was not a valid invoice PDF. These checks do not establish that any supplier portal was automated successfully.

Route exceptions to the right owner

The archive check cannot explain everything. A missing file could mean the document was not issued, the account was restricted, or retrieval failed.

Carry those upstream states into the manifest rather than converting every missing item into another browser retry. Login renewal belongs with account access; an unissued document needs a later check; a listed document with a failed download needs retrieval recovery.

This is also why I would begin with one portal and explicit account, period and document inputs. A reusable Bot or Agent workflow can own that task; a higher-level process can reconcile the supplier outputs. The local check remains useful whichever browser tool produced the files.

Repeat the same period before scheduling it

Run the collection against a known set, inspect the documents, then run the same period again. Check duplicates, unchanged files and unresolved items. Only then add the recurring trigger and an owner for exceptions.

The fuller workflow, including intake routes and original-document handoff, is in our invoice collection guide.

Disclosure: this is a BrowserAct team article prepared with AI assistance. It presents application-side checks and synthetic examples, not a customer deployment or a portal-download benchmark.

Top comments (0)