Tutorial · Use Case

How to Automatically Parse Invoices into QuickBooks Online

By Othmane M., founder of DocuParse API

2026-06-08 · 7 min readLast updated: 2026-10-20

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
AI
checked results
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, time, requests

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",
}


API = "https://docuparseapi.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOCUPARSE_API_KEY']}"}

def parse_document(path: str) -> dict:
    # 1. Submit
    with open(path, "rb") as f:
        submit = requests.post(f"{API}/extract", headers=HEADERS,
                               files={"file": (os.path.basename(path), f)}, timeout=30).json()
    if not submit.get("success"):
        raise RuntimeError(f"[{submit['error']['code']}] {submit['error']['message']}")

    # 2. Fetch the result
    for _ in range(40):
        doc = requests.get(f"{API}/documents/{submit['document_id']}",
                           headers=HEADERS, timeout=30).json()
        if doc["status"] == "completed":
            return doc["data"]
        if doc["status"] == "failed":
            raise RuntimeError(f"Extraction failed: {submit['document_id']}")
        time.sleep(1.5)
    raise TimeoutError(submit["document_id"])


def find_or_create_vendor(vendor_name: str) -> str:
    # Search
    safe = vendor_name.replace("'", "\\'")
    query = f"SELECT * FROM Vendor WHERE DisplayName LIKE '%{safe}%'"
    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["line_total"] or (float(item["price"] or 0) * (item["quantity"] or 1))),
            "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"),
        "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 = parse_document(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"),
    }
DocuParse API → QuickBooks Online field mapping
DocuParse API fieldDestination fieldNote
merchant→VendorRef.nameMatched or created
invoice_id→DocNumberDedup key
date→TxnDateISO 8601
due_date→DueDatenull → omitted
total→TotalAmtFloat conversion
line_items[]→Line[]One Line per item
line_items[].description→Line[].Description
line_items[].total→Line[].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: "DocuParse API 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 };
  }
}

Attach the original PDF

QuickBooks stores the PDF as an Attachable linked to the bill, so the original is one click away when someone reviews it. Call this with the bill ID returned by create_qbo_bill:

python · 19 lines
import json

def attach_pdf_to_bill(bill_id, pdf_path):
    meta = {
        "AttachableRef": [{"EntityRef": {"type": "Bill", "value": bill_id}}],
        "FileName": os.path.basename(pdf_path),
        "ContentType": "application/pdf",
    }
    with open(pdf_path, "rb") as f:
        r = requests.post(
            f"{QBO_BASE}/upload",
            headers={"Authorization": QBO_HEADERS["Authorization"], "Accept": "application/json"},
            files={
                "file_metadata_0": (None, json.dumps(meta), "application/json"),
                "file_content_0": (os.path.basename(pdf_path), f, "application/pdf"),
            },
        )
    r.raise_for_status()
    return r.json()

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

All posts →