An invoice API response is not a document delivery receipt
An integration can deliver the correct invoice number, totals and taxes while leaving the requested document archive empty. If the job has one completed flag, the data importer can accidentally close the document request.
The fix starts with two acceptance contracts. Treat invoice data and an invoice file as separate deliverables, even when both belong to the same business record. A successful data response should satisfy only the requirement it actually proves.
I work on BrowserAct's content and workflow documentation. This is an AI-assisted design example derived from our SRS guide, with synthetic local fixtures. It is not an SRS integration test, an authenticated Roof Hub download, or evidence of a working BrowserAct collector.
Why SRS makes the boundary concrete
The source guide distinguishes Roof Hub's job and invoice history, the supplier-specific Billtrust Invoice Gateway, and the SIPS invoice-data API. Those routes can help with different tasks. The documented API data does not by itself establish delivery of supplier-original PDF files.
That distinction changes the orchestration model. A finance request for structured values can be complete without a PDF requirement. A request for both values and an archived invoice needs two successful outcomes. Do not force every task to collect both; make the requested deliverables explicit at intake.
This example deliberately uses our own internal record shape. It does not claim that SRS returns these field names, exposes a document endpoint, or supports a particular bulk-download limit.
Keep identity and evidence together
Use a stable business reference that includes the authorized customer account and invoice identity. A job name alone is too broad: several invoices can belong to one project. An invoice number without account scope can also be ambiguous in a multi-account collector.
For a file receipt, retain the requested identity, saved bytes' digest, and archive location. Associate the receipt with an identifier check performed on the saved document. A filename or a 200 response alone should not create that receipt.
The identity_checked value below is an input from that separate review step. The function does not inspect a PDF's contents, establish supplier authenticity, or validate the review. It only demonstrates how an orchestrator can refuse to substitute one kind of evidence for another.
Validate each required deliverable
from hashlib import sha256 def acceptance(expected, data, receipt, saved_bytes, required): identity = (expected["account_ref"], expected["invoice_ref"]) data_ok = ( isinstance(data, dict) and (data.get("account_ref"), data.get("invoice_ref")) == identity and data.get("schema_version") == "internal-v1" and data.get("accepted_for_import") is True ) file_ok = ( isinstance(receipt, dict) and (receipt.get("account_ref"), receipt.get("invoice_ref")) == identity and receipt.get("artifact_type") == "supplier_invoice" and receipt.get("identity_checked") is True and isinstance(receipt.get("archive_ref"), str) and bool(receipt["archive_ref"].strip()) and isinstance(saved_bytes, bytes) and bool(saved_bytes) and receipt.get("sha256") == sha256(saved_bytes).hexdigest() ) checks = {"invoice_data": data_ok, "invoice_file": file_ok} if not required or not set(required) <= checks.keys(): raise ValueError("Declare supported deliverables explicitly") missing = [name for name in required if not checks[name]] return {"complete": not missing, "missing": missing} The import flag represents a separate schema and business-validation stage. It is not a replacement for validating currency, decimal precision, tax treatment or source-to-target mappings. Keep those rules in the importer rather than duplicating them in the document worker.
Similarly, a matching digest establishes that the checked receipt refers to these bytes. It does not establish that those bytes are a valid invoice. Document validation belongs upstream of issuing a trusted receipt. In a production service, workers should not be able to mark their own unverified output as reviewed simply by setting a boolean.
Retry the missing deliverable
When data succeeds and the file fails, retain the accepted data result and queue only the missing document work. Conversely, a saved document should survive a temporary import failure. The aggregate task can remain incomplete without discarding either successful result.
Record failure reasons separately: account scope unavailable, invoice not yet listed, document unreadable, save failed, or mapping rejected. Those outcomes need different follow-up. A generic retry of the entire account can waste work and obscure which requirement was never met.
For repeated collection, compare the expected identity set with accepted receipts. Preserve a new document version when bytes change rather than overwriting its previous receipt. Define how a supplier correction changes the expected set before treating a matching invoice number as proof of freshness.
What the local checks establish
The accompanying synthetic checks exercise data-only acceptance, a missing file, a valid pair, account mismatch, a delivery document substituted for an invoice, changed bytes and unsupported requirements. They make no network calls and use placeholder bytes rather than a real invoice PDF.
The design question is small but consequential: which evidence is allowed to close which requirement? Make that relationship explicit before adding browser automation, retries or a scheduler.
Source and portal-specific limits: Access SRS invoices in Roof Hub and Billtrust. The public documentation summarized there supports the data-versus-document distinction; this validation code is an illustrative design, not a supplier-provided API client.