IntegrationsXero
x
Accounting

Automate Xero Invoice Entry — Extract PDF Invoice Data Directly into Xero

Import supplier invoices into Xero without manual data entry. One script, any invoice format.

Trusted by developers worldwide to automate document workflows.

Get Your Free API KeyView API Docs
Free DocuParseAPI accountXero API credentialsPython or Node

The guide

1
Get your credentials
You need: DocuParseAPI key, Xero OAuth 2.0 access token, and your Xero Tenant ID. Create a Xero app at developer.xero.com. Use the Xero OAuth 2.0 Playground to get an initial access token for testing.
2
Extract the invoice
POST the PDF to DocuParseAPI. The response maps directly to Xero fields:
merchant  →  Contact.Name
date      →  Date
due_date  →  DueDate
invoice_id → InvoiceNumber
currency  →  CurrencyCode
line_items → LineItems[] array
3
Find or create the Xero Contact
Search Xero Contacts for the merchant name. Create if not found:
def get_or_create_contact(name, base, headers):
    r = requests.get(f"{base}/Contacts", headers=headers,
                     params={"searchTerm": name})
    contacts = r.json().get("Contacts", [])
    if contacts: return contacts[0]["ContactID"]
    r = requests.post(f"{base}/Contacts", headers=headers,
                      json={"Contacts": [{"Name": name}]})
    return r.json()["Contacts"][0]["ContactID"]
Need your API key first?
20 free documents/month — no credit card
Get Free API Key →
4
Create a Draft Bill (ACCPAY)
POST to Xero /Invoices with Type: ACCPAY and Status: DRAFT so a human can approve before payment:
bill = {
    "Type": "ACCPAY",
    "Contact": {"ContactID": contact_id},
    "Date": invoice["date"],
    "DueDate": invoice["due_date"],
    "InvoiceNumber": invoice["invoice_id"],
    "CurrencyCode": invoice.get("currency", "USD"),
    "Status": "DRAFT",
    "LineItems": [{
        "Description": item["description"],
        "Quantity": item.get("quantity", 1),
        "UnitAmount": float(item.get("unit_price") or item.get("amount") or 0),
        "AccountCode": "429"
    } for item in invoice.get("line_items", [])]
}
5
Check for validation errors
Xero returns HasValidationErrors: true if the bill is invalid. Always check this before proceeding:
result = r.json()["Invoices"][0]
if result.get("HasValidationErrors"):
    raise RuntimeError(f"Xero error: {result['ValidationErrors']}")
print(f"Bill created: {result['InvoiceID']}")

Full working example

python
import os, requests

XERO_BASE = "https://api.xero.com/api.xro/2.0"
HEADERS = {"Authorization": f"Bearer {os.environ['XERO_ACCESS_TOKEN']}",
           "Xero-tenant-id": os.environ['XERO_TENANT_ID'],
           "Content-Type": "application/json", "Accept": "application/json"}
ACCOUNT_CODE = os.environ.get("XERO_EXPENSE_ACCOUNT_CODE", "429")

def extract_invoice(path):
    with open(path, "rb") as f:
        r = requests.post("https://docuparseapi.com/api/v1/extract",
            headers={"Authorization": f"Bearer {os.environ['DOCUPARSE_API_KEY']}"},
            files={"file": f}, timeout=30)
    data = r.json()
    if not data["success"]: raise RuntimeError(data["error"]["code"])
    return data

def get_or_create_contact(name):
    r = requests.get(f"{XERO_BASE}/Contacts", headers=HEADERS, params={"searchTerm": name})
    contacts = r.json().get("Contacts", [])
    if contacts: return contacts[0]["ContactID"]
    r = requests.post(f"{XERO_BASE}/Contacts", headers=HEADERS,
                      json={"Contacts": [{"Name": name}]})
    return r.json()["Contacts"][0]["ContactID"]

def process(file_path):
    inv = extract_invoice(file_path)
    contact_id = get_or_create_contact(inv.get("merchant") or "Unknown")
    r = requests.post(f"{XERO_BASE}/Invoices", headers=HEADERS,
        json={"Invoices": [{
            "Type": "ACCPAY", "Contact": {"ContactID": contact_id},
            "Date": inv.get("date"), "DueDate": inv.get("due_date"),
            "InvoiceNumber": inv.get("invoice_id"),
            "CurrencyCode": inv.get("currency", "USD"), "Status": "DRAFT",
            "LineItems": [{"Description": i.get("description",""),
                "Quantity": i.get("quantity",1),
                "UnitAmount": float(i.get("unit_price") or i.get("amount") or 0),
                "AccountCode": ACCOUNT_CODE}
                for i in inv.get("line_items",[])]}
        ]})
    result = r.json()["Invoices"][0]
    if result.get("HasValidationErrors"):
        raise RuntimeError(result["ValidationErrors"])
    print(f"Xero Bill created: {result['InvoiceID']}")

process("invoice.pdf")

What gets extracted

FieldDescriptionDestination field
merchantThe vendor or store nameContact.Name
totalThe final charged amount including taxCalculated from lines
subtotalThe pre-tax amountLineItems[].UnitAmount
taxThe tax amount chargedTaxType on lines
dateInvoice or transaction date (ISO 8601)Date
due_datePayment due date, or null if not presentDueDate
invoice_idInvoice number or receipt ID from the documentInvoiceNumber
currencyISO 4217 currency code (USD, EUR, GBP, etc.)CurrencyCode
payment_methodCard type, cash, or other payment methodNot mapped
line_itemsArray of items with description, quantity, unit_price, totalLineItems[] array

Common errors and fixes

MISSING_API_KEYAuthorization header missingAdd header: Authorization: Bearer YOUR_KEY
INVALID_API_KEYKey not recognisedCheck key in dashboard — regenerate if needed
LIMIT_EXCEEDEDMonthly document limit reachedUpgrade at docuparseapi.com/pricing
UNSUPPORTED_FILE_TYPEFile format not supportedUse PDF, JPG, or PNG only
FILE_TOO_LARGE_AUTHENTICATEDFile exceeds 10 MBCompress the file before sending
EXTRACTION_FAILEDDocument could not be parsedTry a cleaner scan or different file

Ready to start?

20 documents free every month. No credit card. Set up in 5 minutes.

Get Your Free API Key