This playbook covers the end-to-end invoice-to-pay integration pattern connecting three system categories: OCR/AI invoice capture platforms (Kofax, ABBYY, Rossum, Coupa, Stampli), ERP AP modules (SAP, Oracle, NetSuite, Dynamics 365), and payment gateways (Tipalti, AvidXchange, Bill.com, Corpay). The architecture is vendor-agnostic — the integration pattern, data mapping, and failure handling apply regardless of specific vendor combination.
This card does NOT cover: upstream procurement/PO creation, expense management (T&E), or accounts receivable automation.
| System | Role | API Surface | Direction |
|---|---|---|---|
| OCR/AI Platform (Kofax, ABBYY, Rossum, Stampli) | Invoice ingestion, data extraction, confidence scoring | REST API | Outbound to middleware |
| ERP AP Module (SAP, Oracle, NetSuite, D365) | Master data (vendors, GL, POs), 3-way match, GL posting | REST/OData/SOAP | Inbound (invoices) + Outbound (PO/GRN data) |
| Approval Engine (ERP-native or Coupa, Stampli) | Routing, threshold-based approvals, exception handling | REST API or ERP-native | Bidirectional |
| Payment Gateway (Tipalti, AvidXchange, Bill.com) | Payment execution (ACH, wire, check, virtual card) | REST API | Inbound (approved invoices) + Outbound (payment status) |
| Middleware/iPaaS (MuleSoft, Boomi, Workato, Celigo) | Orchestration, transformation, error handling, retry | N/A | Orchestrator |
| Component | Protocol | Best For | Throughput | Real-time? | Bulk? |
|---|---|---|---|---|---|
| OCR extraction API (Rossum, ABBYY) | HTTPS/JSON | Single invoice extraction | 1-5 sec/page | Yes | Batch mode available |
| ERP vendor master lookup | REST/OData | Vendor ID resolution | 50-200 req/sec | Yes | No |
| ERP PO/GRN query | REST/OData | 3-way match data retrieval | 50-200 req/sec | Yes | Pagination required |
| ERP AP invoice posting | REST/SOAP | GL journal entry creation | 10-50 invoices/sec | Yes | Batch posting preferred |
| Payment gateway submission | HTTPS/JSON | Payment batch execution | 1,000+ payments/batch | Batch (daily cutoff) | Yes |
| Payment status webhook | HTTPS/JSON | Payment confirmation | Event-driven | Yes (webhook) | N/A |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| OCR pages per request | 1-50 pages | ABBYY, Rossum | Batch endpoints accept multi-page PDFs |
| ERP AP invoice line items | 200-1,000 lines | SAP, Oracle, NetSuite | Split mega-invoices before posting |
| Payment batch size | 1,000-10,000 payments | Tipalti, AvidXchange | Larger batches require pre-approval |
| Attachment size | 10-25 MB | Most platforms | Compress scans before upload |
| Limit Type | Value | Window | Platform Differences |
|---|---|---|---|
| OCR API calls | 5,000-50,000/day | 24h | Depends on subscription tier |
| ERP API calls | 10,000-100,000/day | 24h rolling | SAP: fair-use throttle; Salesforce: 100K; NetSuite: governance units |
| Payment batches | 1-4 per day | Daily cutoff | Tipalti: 4pm ET ACH cutoff; AvidXchange: 2pm ET |
| Webhook deliveries | Unlimited (push) | Per event | Retry 3x with exponential backoff |
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 Client Credentials | Server-to-server ERP + payment gateway | 1-2 hours | Yes (auto) | Recommended for all production integrations |
| API Key + Secret | OCR platform integration | Permanent until rotated | N/A | Rotate every 90 days minimum |
| OAuth 2.0 Authorization Code | User-context approval workflows | 1h access / long-lived refresh | Yes | Required when integration acts on behalf of approvers |
| Certificate-based (mTLS) | SAP S/4HANA on-premise, bank connections | Session-based | New cert per session | Required for some payment rails |
START — Need to automate invoice-to-pay AP process
├── What invoice types?
│ ├── PO-backed invoices (3-way match required)
│ │ ├── Volume < 500/month → Embedded AP tool (Stampli, Ramp, BILL)
│ │ ├── Volume 500-5,000/month → Mid-market platform (Tipalti, AvidXchange)
│ │ └── Volume > 5,000/month → Enterprise OCR + iPaaS + ERP + payment gateway
│ ├── Non-PO invoices (GL coding required)
│ │ ├── AI/ML GL coding available → Auto-code with confidence threshold → approval
│ │ └── No AI coding → Manual coding → approval workflow
│ └── Recurring invoices → Create schedule in ERP → auto-match → skip approval if within tolerance
├── What ERP system?
│ ├── SAP S/4HANA → SAP Invoice Management or Kofax + SAP API
│ ├── Oracle ERP Cloud → Oracle AP module + FBDI or REST API
│ ├── NetSuite → Embedded AP or SuiteScript + REST API
│ └── Dynamics 365 → D365 vendor invoice automation or third-party + OData
├── Payment execution?
│ ├── Domestic only (US ACH + check) → BILL, AvidXchange
│ ├── Global payments (multi-currency) → Tipalti, Corpay
│ └── Virtual card (rebate capture) → Platform with vCard support
└── Error tolerance?
├── Zero tolerance (regulated) → Full 3-way match + dual approval + audit trail
└── Standard tolerance (±2-5%) → Auto-approve within tolerance, exception queue for outliers
| Step | Source System | Action | Target System | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1. Ingest | Email / SFTP / Scanner | Invoice arrives (PDF, image, XML, EDI) | OCR Platform | Raw document | Dead letter queue for unreadable files |
| 2. Extract | OCR Platform | AI extracts header + line items with confidence scores | Middleware | Extracted invoice JSON | Low-confidence fields → human review queue |
| 3. Validate | Middleware | Vendor lookup, duplicate check, field validation | ERP (vendor master) | Vendor ID, invoice number | Unknown vendor → onboarding workflow |
| 4. Match | Middleware / ERP | 3-way match: Invoice vs PO vs GRN | ERP (PO + GRN tables) | PO lines, GRN lines, tolerances | Mismatch → exception queue with variance details |
| 5. Code GL | AP Platform / AI | Auto-assign GL accounts, cost centers, tax codes | ERP (chart of accounts) | GL codes, tax codes | Low-confidence coding → manual review |
| 6. Approve | Approval Engine | Route based on amount thresholds and department rules | ERP or AP Platform | Approval status, approver ID | Escalation after SLA breach (48h default) |
| 7. Post | Middleware | Create AP invoice / journal entry in ERP | ERP AP Module | AP voucher, GL entries | Posting failure → retry 3x, then alert |
| 8. Schedule | ERP / Payment Platform | Queue for payment based on terms and cash position | Payment Gateway | Payment batch | Missed cutoff → next business day |
| 9. Execute | Payment Gateway | Execute ACH, wire, check, or virtual card payment | Bank / Vendor | Payment confirmation | Failed payment → retry + vendor notification |
| 10. Reconcile | Payment Gateway | Payment status webhook → update ERP | ERP AP Module | Payment reference, clearing date | Unmatched payment → manual reconciliation |
Set up the OCR platform to accept invoices from all ingestion channels (email forwarding, SFTP upload, API submission) and extract structured data. [src2]
# Example: Rossum invoice extraction via REST API
import requests
ROSSUM_API_KEY = "your_api_key"
ROSSUM_QUEUE_ID = "123456"
def submit_invoice_to_ocr(file_path: str) -> dict:
"""Submit invoice PDF to OCR and get extraction results."""
url = f"https://elis.rossum.ai/api/v1/queues/{ROSSUM_QUEUE_ID}/upload"
headers = {"Authorization": f"Bearer {ROSSUM_API_KEY}"}
with open(file_path, "rb") as f:
response = requests.post(url, headers=headers, files={"content": f})
response.raise_for_status()
annotation_url = response.json()["results"][0]["annotation"]
return poll_annotation(annotation_url, headers)
Verify: Check that extracted fields include vendor_name, invoice_number, invoice_date, total_amount, line_items[] with confidence scores above 0.80.
Before 3-way matching, validate the extracted vendor against the ERP vendor master. Resolve vendor ID, payment terms, and default GL coding. [src5]
# Example: NetSuite vendor lookup via SuiteQL
def lookup_vendor_in_erp(vendor_name: str, tax_id: str = None) -> dict:
"""Resolve vendor in ERP master data."""
if tax_id:
query = f"SELECT id, entityid, companyname FROM vendor WHERE taxidnum = '{tax_id}'"
else:
query = f"SELECT id, entityid, companyname FROM vendor WHERE companyname LIKE '%{vendor_name}%' LIMIT 5"
response = requests.post(
f"{NETSUITE_URL}/services/rest/query/v1/suiteql",
headers={"Authorization": f"Bearer {access_token}"},
json={"q": query}
)
results = response.json().get("items", [])
if not results:
raise VendorNotFoundError(f"No vendor match for: {vendor_name} / {tax_id}")
return results[0]
Verify: Vendor ID resolved, payment terms and default GL account returned. If no match, invoice routes to vendor onboarding queue.
Compare invoice header and line items against the purchase order and goods receipt note. Apply tolerance thresholds for auto-approval. [src3, src4]
def three_way_match(invoice, po, grn, tolerances) -> dict:
"""
3-way match with configurable tolerances.
tolerances = {"price_pct": 0.02, "qty_pct": 0.05, "total_abs": 100.00}
"""
variances = []
for inv_line in invoice["line_items"]:
po_line = find_matching_po_line(inv_line, po["lines"])
grn_line = find_matching_grn_line(inv_line, grn["lines"])
if not po_line:
variances.append({"line": inv_line["line_num"], "type": "NO_PO_MATCH", "severity": "high"})
continue
price_diff = abs(inv_line["unit_price"] - po_line["unit_price"]) / po_line["unit_price"]
if price_diff > tolerances["price_pct"]:
variances.append({"line": inv_line["line_num"], "type": "PRICE_VARIANCE",
"variance_pct": price_diff})
if grn_line:
qty_diff = abs(inv_line["quantity"] - grn_line["received_qty"]) / grn_line["received_qty"]
if qty_diff > tolerances["qty_pct"]:
variances.append({"line": inv_line["line_num"], "type": "QTY_VARIANCE",
"variance_pct": qty_diff})
high_severity = [v for v in variances if v.get("severity") == "high" or v.get("variance_pct", 0) > tolerances["price_pct"]]
return {"status": "exception" if high_severity else "matched", "variances": variances}
Verify: Matched invoices return status: "matched". Exception invoices have detailed variance records for human review.
After approval, create the AP invoice/voucher in the ERP with full GL coding, tax allocation, and payment terms. [src2, src5]
# Example: SAP S/4HANA AP invoice posting via API
def post_invoice_to_erp(approved_invoice):
payload = {
"CompanyCode": approved_invoice["company_code"],
"InvoicingParty": approved_invoice["vendor_id"],
"DocumentDate": approved_invoice["invoice_date"],
"PostingDate": approved_invoice["posting_date"],
"SupplierInvoiceIDByInvcgParty": approved_invoice["invoice_number"],
"InvoiceGrossAmount": str(approved_invoice["total_amount"]),
"DocumentCurrency": approved_invoice["currency"],
"to_SuplrInvcItemPurOrdRef": [
{"PurchaseOrder": line["po_number"], "PurchaseOrderItem": line["po_line"],
"SupplierInvoiceItemAmount": str(line["amount"]), "TaxCode": line["tax_code"],
"GLAccount": line["gl_account"], "CostCenter": line["cost_center"]}
for line in approved_invoice["line_items"]
]
}
response = requests.post(
f"{SAP_API_URL}/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice",
headers={"Authorization": f"Bearer {token}"}, json=payload)
response.raise_for_status()
return response.json()
Verify: ERP returns AP document number. GL entries visible in trial balance. Payment due date calculated from payment terms.
Collect approved, posted invoices due for payment and submit to the payment gateway for execution. [src1, src5]
# Example: Tipalti payment batch submission
def submit_payment_batch(invoices, payment_date):
payments = [{"payeeId": inv["vendor_external_id"],
"amountSubmitted": inv["payment_amount"],
"currency": inv["currency"],
"invoiceRefCode": inv["erp_document_number"]}
for inv in invoices]
response = requests.post(f"{TIPALTI_API_URL}/api/v1/payments/batch",
headers={"Authorization": f"Bearer {tipalti_token}"},
json={"payments": payments, "paymentDate": payment_date})
response.raise_for_status()
return response.json()
Verify: Payment gateway returns batch ID. Monitor status via webhook or polling until all payments reach completed status.
When payments execute, update the ERP AP module to clear the open invoice and record the payment reference. [src5]
def handle_payment_webhook(webhook_payload):
for payment in webhook_payload["payments"]:
if payment["status"] == "completed":
clear_ap_document(
document_number=payment["invoiceRefCode"],
payment_reference=payment["paymentId"],
clearing_date=payment["executedDate"])
elif payment["status"] == "failed":
create_exception(document_number=payment["invoiceRefCode"],
error=payment["failureReason"], severity="high")
Verify: Open AP items cleared in ERP. Bank reconciliation matches payment gateway records. No orphaned open items.
See Step-by-Step Integration Guide above for complete, runnable code examples covering OCR submission (Python/Rossum), vendor lookup (Python/NetSuite SuiteQL), 3-way matching (Python), ERP posting (Python/SAP), payment batch submission (Python/Tipalti), and webhook reconciliation (Python).
| Source Field (OCR Output) | Target Field (ERP AP Module) | Type | Transform | Gotcha |
|---|---|---|---|---|
| vendor_name | Vendor ID (internal) | String → Lookup | Fuzzy match against vendor master | Name variations (Inc. vs LLC vs Ltd) cause duplicate vendor creation |
| invoice_number | Supplier Invoice Reference | String | Direct (trim whitespace, normalize) | Some vendors reuse invoice numbers across years |
| invoice_date | Document Date | Date | Parse from various formats → ISO 8601 | MM/DD/YYYY vs DD/MM/YYYY confusion |
| due_date | Payment Due Date | Date | Parse or calculate from payment terms | If missing, calculate from invoice_date + terms |
| total_amount | Invoice Gross Amount | Decimal | Extract, validate against line item sum | Currency symbol extraction errors |
| tax_amount | Tax Amount | Decimal | Extract or calculate from rate × base | Tax-inclusive vs tax-exclusive varies by region |
| po_number | Purchase Order Reference | String | Direct match against open POs | Partial PO numbers, prefixed zeros cause failures |
| line_item.description | PO Line Description | String | Fuzzy match against PO line descriptions | OCR truncation on long descriptions |
| line_item.quantity | Invoice Quantity | Decimal | Direct | Unit of measure mismatch (each vs case vs pallet) |
| line_item.unit_price | Invoice Unit Price | Decimal | Direct, convert currency if needed | Rounding differences — use tolerance threshold |
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| OCR_LOW_CONFIDENCE | Field extraction below threshold | Poor scan quality, handwriting | Route to human review; re-scan at higher DPI |
| VENDOR_NOT_FOUND | No vendor match in ERP master | New vendor, name variation | Trigger vendor onboarding workflow |
| PO_NOT_FOUND | Purchase order not found or closed | Wrong PO number extracted, PO closed | Manual review; check for OCR errors |
| GRN_MISSING | Goods receipt not yet posted | Invoice arrived before goods received | Park invoice; auto-retry when GRN posts |
| PRICE_VARIANCE | Unit price exceeds tolerance vs PO | Price change, surcharge included | Route to buyer for confirmation |
| QTY_VARIANCE | Quantity mismatch vs GRN | Partial shipment, damaged goods | Route to receiving department |
| DUPLICATE_INVOICE | Same vendor + invoice number exists | Re-submitted invoice | Block posting; alert AP team |
| GL_POSTING_FAILED | ERP rejected the journal entry | Period closed, invalid GL account | Check fiscal calendar; validate GL coding |
| PAYMENT_REJECTED | Bank rejected payment | Invalid bank details, sanctions screening | Update vendor banking info; retry next batch |
Set minimum field-level confidence threshold (80%+ for amount fields, 90%+ for vendor ID) and route failures to human review. [src6]Require 2-way match (invoice vs GL budget) and dual approval for all non-PO invoices above $500. [src4]Normalize invoice numbers (strip leading zeros, remove alpha suffixes) and check duplicates within 90-day window. [src6]Use idempotency keys on all payment submissions. Check status before retrying. [src1]Lock exchange rate at invoice posting time and pass explicit rate to payment gateway. [src5]Check fiscal period status before batch processing. Implement period-aware queuing. [src2]# BAD — posts whatever OCR extracts, no matter the confidence
def auto_post_invoice(ocr_result):
erp_client.post_invoice({
"vendor": ocr_result["vendor_name"], # Not resolved to vendor ID
"amount": ocr_result["total"], # No confidence check
"po": ocr_result["po_number"] # No PO validation
})
# GOOD — validates every field, resolves references, checks confidence
def process_invoice(ocr_result):
if ocr_result["confidence"]["total_amount"] < 0.80:
route_to_human_review(ocr_result)
return
vendor = lookup_vendor(ocr_result["vendor_name"], ocr_result["tax_id"])
po = get_purchase_order(ocr_result["po_number"])
if not po or po["status"] == "closed":
create_exception("PO_NOT_FOUND", ocr_result)
return
match_result = three_way_match(ocr_result, po, get_grn(po["id"]))
if match_result["status"] == "exception":
route_to_exception_queue(ocr_result, match_result)
return
erp_client.post_invoice(build_erp_payload(ocr_result, vendor, po))
# BAD — processes payments one at a time, slow and expensive
for invoice in approved_invoices:
payment_gateway.submit_payment(invoice)
time.sleep(1) # Rate limit workaround
# GOOD — batches payments, respects cutoff times, uses idempotency keys
import hashlib
from datetime import datetime, time as dtime
def submit_daily_payment_batch(approved_invoices):
cutoff = dtime(16, 0) # 4:00 PM ET for same-day ACH
if datetime.now().time() > cutoff:
log.warning("Past cutoff — payments execute next business day")
batch = []
for inv in approved_invoices:
key = hashlib.sha256(
f"{inv['vendor_id']}:{inv['invoice_number']}:{inv['amount']}".encode()
).hexdigest()
batch.append({**inv, "idempotency_key": key})
return payment_gateway.submit_batch(batch)
# BAD — assumes entire batch succeeded or failed
result = erp_client.post_invoice_batch(invoices)
if result.status_code == 200:
mark_all_as_posted(invoices) # Some may have failed individually
# GOOD — tracks individual record outcomes within a batch
result = erp_client.post_invoice_batch(invoices)
for i, item_result in enumerate(result["items"]):
if item_result["status"] == "success":
mark_as_posted(invoices[i], item_result["document_number"])
else:
create_exception(invoices[i], item_result["error"])
Implement deduplication rules using tax ID as primary key; merge duplicates before go-live. [src6]Start with 5% price / 10% quantity tolerance for first 90 days; tighten to 2% / 5% after exception patterns stabilize. [src4]Generate document fingerprint (hash of vendor + amount + date) on first ingestion; check before creating new record. [src6]Use business day calendar for due date calculation; account for bank holidays in payment scheduling. [src1]Require mandatory reason codes and comments for all exception overrides. [src7]Load test with production-scale volumes; monitor API consumption during tests; implement batch chunking. [src8]# Check OCR extraction quality for a specific invoice
curl -X GET "https://elis.rossum.ai/api/v1/annotations/{annotation_id}" \
-H "Authorization: Bearer $ROSSUM_TOKEN" \
| jq '.content[] | {field: .schema_id, value: .value, confidence: .confidence}'
# Query ERP for open POs awaiting invoice match (NetSuite example)
curl -X POST "$NETSUITE_URL/services/rest/query/v1/suiteql" \
-H "Authorization: Bearer $NS_TOKEN" \
-d '{"q": "SELECT tranid, entity, total FROM transaction WHERE type = '\''PurchOrd'\'' AND status = '\''open'\''"}'
# Check for duplicate invoices in ERP before posting
curl -X POST "$NETSUITE_URL/services/rest/query/v1/suiteql" \
-H "Authorization: Bearer $NS_TOKEN" \
-d '{"q": "SELECT tranid, total FROM transaction WHERE type = '\''VendBill'\'' AND tranid = '\''INV-12345'\''"}'
# Check payment batch status in Tipalti
curl -X GET "https://api.tipalti.com/api/v1/payments/batch/{batch_id}/status" \
-H "Authorization: Bearer $TIPALTI_TOKEN"
# Monitor 3-way match exception queue depth
curl -X GET "$AP_PLATFORM_URL/api/v1/exceptions?status=open" \
-H "Authorization: Bearer $AP_TOKEN" | jq '.total_count'
# Verify GL posting in ERP (SAP S/4HANA example)
curl -X GET "$SAP_URL/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice('5105600001')" \
-H "Authorization: Bearer $SAP_TOKEN"
| Component | Version/Release | Date | Status | Breaking Changes |
|---|---|---|---|---|
| Rossum API | v3 | 2025-11 | Current | Schema validation changes on extraction output |
| ABBYY Vantage | 2.5 | 2025-09 | Current | None |
| Kofax TotalAgility | 8.0 | 2025-06 | Current | New connector framework replaces legacy adapters |
| Tipalti API | v6 | 2025-10 | Current | Payment batch endpoint changed from /bills to /payments |
| AvidXchange API | v3 | 2025-08 | Current | OAuth 2.0 required (API key deprecated) |
| Bill.com API | v3 | 2025-07 | Current | Pagination changed to cursor-based |
| SAP S/4HANA Cloud | 2408 | 2024-08 | Current | New AP API endpoints for 2-step verification |
| Oracle ERP Cloud | 24B | 2024-06 | Current | Invoice REST API v2 replaces v1 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| PO-backed invoice processing with 3-way match requirements | Expense reimbursement or T&E processing | Expense management platforms (Concur, Expensify) |
| Volume exceeds 500 invoices/month | Fewer than 100 invoices/month with simple GL coding | Basic AP module in accounting software |
| Multi-vendor, multi-currency AP operations | Single vendor, single currency recurring payments | Scheduled bank transfers or standing orders |
| SOX compliance requires documented approval trails | Non-regulated small business with owner-approval | Simple AP workflow in QuickBooks/Xero |
| Need to capture early payment discounts at scale | All vendors on Net 30 with no discount incentives | Standard ERP payment run |
| Capability | Kofax / ABBYY | Rossum | Coupa | Stampli | Tipalti |
|---|---|---|---|---|---|
| OCR/AI Extraction | Enterprise-grade, 99%+ header accuracy | AI-native, learns per customer | Built-in invoice capture | AI-assisted capture | Basic capture included |
| ERP Connectors | 200+ pre-built | 50+ via marketplace | SAP, Oracle, NetSuite, D365 | 70+ connectors | SAP, NetSuite, D365, Sage |
| 3-Way Matching | Via ERP integration | Via ERP integration | Native (Coupa PO module) | Native with AI assist | Native with PO matching |
| GL Coding AI | ML-based coding available | AI auto-coding | Rule + ML hybrid | AI auto-coding | ML-based coding |
| Payment Execution | Via payment gateway | Via payment gateway | Coupa Pay | Via payment gateway | Native (190+ countries) |
| Virtual Card Support | Via partner | Via partner | Coupa Pay | Via partner | Native |
| Pricing Model | Per-page or per-document | Per-document | Platform license | Per-user | Per-payment + platform fee |
| Best For | High-volume enterprise (>10K/mo) | Mid to enterprise (1K-50K/mo) | Procurement-centric orgs | AP team collaboration | Global payments focus |