BlogHow to Automatically Parse Invoices into QuickBooks Online

How to Automatically Parse Invoices into QuickBooks Online

2026-06-08 · 7 min read

QuickBooks Online is where the invoice data needs to live. The problem is that invoices arrive as PDFs, and getting the data from the PDF into QuickBooks currently requires someone to type it there. This guide eliminates that step.

You'll build a system that reads a PDF invoice, extracts the vendor name, invoice number, dates, amounts, and line items, and creates a Bill in QuickBooks Online — automatically, without a human touching a keyboard.

Auto
bill creation
0
data entry
~3s
extraction
Free
to start
Prerequisites

What You'll Need

See the exact data that goes into QuickBooks
Upload a supplier invoice. See the merchant, total, and line items that become a Bill.
Open Live Demo →
Free tier · 20 documents/month — free forever · No credit card · No account needed for the demo
Python · Node.js

How QuickBooks Bills Work

In QuickBooks Online, a Bill represents a supplier invoice — money you owe a vendor. Bills have:

  • A Vendor (must exist as a contact in QBO)
  • A transaction date and due date
  • Line items with amounts and account codes
  • A document number (your invoice reference)

The extracted invoice data maps directly to these fields.

Node.js · Python
import os
import requests

DOCUPARSE_KEY = os.environ["DOCUPARSE_API_KEY"]
QBO_TOKEN = os.environ["QBO_ACCESS_TOKEN"]
QBO_COMPANY_ID = os.environ["QBO_COMPANY_ID"]
QBO_BASE = f"https://quickbooks.api.intuit.com/v3/company/{QBO_COMPANY_ID}"
QBO_HEADERS = {
    "Authorization": f"Bearer {QBO_TOKEN}",
    "Accept": "application/json",
    "Content-Type": "application/json",
}


def extract_invoice(file_path: str) -> dict:
    with open(file_path, "rb") as f:
        response = requests.post(
            "https://docuparseapi.com/api/v1/extract",
            headers={"Authorization": f"Bearer {DOCUPARSE_KEY}"},
            files={"file": f},
            timeout=30,
        )
    data = response.json()
    if not data["success"]:
        raise RuntimeError(f"[{data['error']['code']}] {data['error']['message']}")
    return data


def find_or_create_vendor(vendor_name: str) -> str:
    # Search
    query = f"SELECT * FROM Vendor WHERE DisplayName LIKE '%{vendor_name}%'"
    response = requests.get(
        f"{QBO_BASE}/query",
        headers=QBO_HEADERS,
        params={"query": query, "minorversion": "65"},
    )
    vendors = response.json().get("QueryResponse", {}).get("Vendor", [])
    if vendors:
        return vendors[0]["Id"]

    # Create
    response = requests.post(
        f"{QBO_BASE}/vendor",
        headers=QBO_HEADERS,
        params={"minorversion": "65"},
        json={"DisplayName": vendor_name, "PrintOnCheckName": vendor_name},
    )
    return response.json()["Vendor"]["Id"]


def create_qbo_bill(invoice_data: dict, vendor_id: str) -> dict:
    lines = [
        {
            "Amount": float(item.get("total") or item.get("amount") or 0),
            "DetailType": "AccountBasedExpenseLineDetail",
            "Description": item.get("description", ""),
            "AccountBasedExpenseLineDetail": {
                "AccountRef": {"value": os.environ.get("QBO_EXPENSE_ACCOUNT_ID", "7")}
            },
        }
        for item in invoice_data.get("line_items", [])
    ] or [{
        "Amount": float(invoice_data.get("subtotal") or invoice_data.get("total") or 0),
        "DetailType": "AccountBasedExpenseLineDetail",
        "Description": f"Invoice {invoice_data.get('invoice_id', '')}",
        "AccountBasedExpenseLineDetail": {
            "AccountRef": {"value": os.environ.get("QBO_EXPENSE_ACCOUNT_ID", "7")}
        },
    }]

    bill = {
        "VendorRef": {"value": vendor_id},
        "TxnDate": invoice_data.get("date"),
        "DueDate": invoice_data.get("due_date"),
        "DocNumber": invoice_data.get("invoice_id"),
        "TotalAmt": float(invoice_data.get("total") or 0),
        "Line": lines,
    }

    response = requests.post(
        f"{QBO_BASE}/bill",
        headers=QBO_HEADERS,
        params={"minorversion": "65"},
        json=bill,
    )
    response.raise_for_status()
    return response.json()["Bill"]


def process_invoice(file_path: str) -> dict:
    extracted = extract_invoice(file_path)
    merchant = extracted.get("merchant") or "Unknown Vendor"
    vendor_id = find_or_create_vendor(merchant)
    bill = create_qbo_bill(extracted, vendor_id)

    return {
        "bill_id": bill["Id"],
        "vendor": merchant,
        "total": extracted.get("total"),
        "currency": extracted.get("currency"),
    }
DocuParseAPI → QuickBooks Online field mapping
DocuParseAPI fieldDestination fieldNote
merchantVendorRef.nameMatched or created
invoice_idDocNumberDedup key
dateTxnDateISO 8601
due_dateDueDatenull → omitted
totalTotalAmtFloat conversion
line_items[]Line[]One Line per item
line_items[].descriptionLine[].Description
line_items[].totalLine[].AmountFloat conversion
Your QuickBooks integration is one API key away.
20 documents/month — free forever. No templates. Straight to QuickBooks Bills.
Dedup

Setting the Correct Expense Account

The AccountRef.value in line items must match an account ID in your QuickBooks chart of accounts. To find the right ID:

javascript · 10 lines
// List all expense accounts
const query = "SELECT * FROM Account WHERE AccountType = 'Expense'";
const response = await fetch(
  `${QBO_BASE}/query?query=${encodeURIComponent(query)}&minorversion=65`,
  { headers: { Authorization: `Bearer ${QBO_ACCESS_TOKEN}`, Accept: "application/json" } }
);
const data = await response.json();
data.QueryResponse.Account.forEach(acc => {
  console.log(`${acc.Id}: ${acc.Name}`);
});

Set the ID as QBO_EXPENSE_ACCOUNT_ID in your environment variables.

Errors

Error Handling for Production

javascript · 16 lines
async function processSafely(filePath) {
  try {
    return await processPDFInvoice(filePath);
  } catch (error) {
    if (error.message.includes("LIMIT_EXCEEDED")) {
      return { status: "error", reason: "DocuParseAPI monthly limit reached" };
    }
    if (error.message.includes("EXTRACTION_FAILED")) {
      return { status: "review", reason: "Could not extract — needs manual review" };
    }
    if (error.message.includes("QBO bill creation failed")) {
      return { status: "error", reason: "QuickBooks API error", detail: error.message };
    }
    return { status: "error", reason: error.message };
  }
}

Full Example

Next Steps

Supplier invoices should go straight to QuickBooks.

No data entry. No templates. 20 documents/month — free forever.

More from the blog