---
# === IDENTITY ===
id: business/erp-integration/procure-to-pay-integration/2026
canonical_question: "How do you implement Procure-to-Pay integration - requisition to PO to receipt to invoice to payment?"
aliases:
  - "P2P integration architecture from purchase requisition to payment"
  - "How to automate procure-to-pay across ERP and procurement systems"
  - "End-to-end purchase order to invoice matching integration"
  - "Requisition-to-payment integration playbook for ERP systems"
entity_type: erp_integration
domain: business > erp-integration > procure-to-pay-integration
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "SAP S/4HANA"
    vendor: "SAP"
    version: "2408"
    edition: "Cloud / On-Premise"
    deployment: hybrid
    api_surface: "OData v4, BAPI/RFC, IDoc"
  - name: "Oracle ERP Cloud"
    vendor: "Oracle"
    version: "24B"
    edition: "Enterprise"
    deployment: cloud
    api_surface: "REST, SOAP, FBDI"
  - name: "NetSuite"
    vendor: "Oracle"
    version: "2024.2"
    edition: "SuiteCloud"
    deployment: cloud
    api_surface: "REST, SuiteTalk SOAP"
  - name: "Microsoft Dynamics 365 Finance"
    vendor: "Microsoft"
    version: "10.0.40"
    edition: "Enterprise"
    deployment: cloud
    api_surface: "OData v4, Data Entities"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.85
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "SAP S/4HANA 2408 — Purchase Order OData v4 API GA, BAPI still supported"
  next_review: 2026-08-29
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "3-way match requires receipt data synced before invoice can be validated — timing gaps cause false mismatches"
  - "PO number mapping across systems must be bidirectional — both systems need cross-reference IDs"
  - "Tax calculation must happen at the correct stage — before or after matching depending on jurisdiction"
  - "Currency conversion rates must use the same source and timestamp across procurement and AP systems"
  - "Tolerance thresholds (price/quantity variance) must be configured identically in both procurement and AP modules"
  - "Supplier master data must be synchronized before any transactional data flows — orphan POs/invoices cause cascading failures"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User only needs AP invoice automation without procurement"
    use_instead: "business/erp-integration/ap-automation-integration/2026"
  - condition: "User needs supplier onboarding/management integration only"
    use_instead: "business/erp-integration/supplier-management-integration/2026"
  - condition: "User needs only purchase requisition approval workflow"
    use_instead: "business/erp-integration/procurement-approval-workflow/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily transaction volume?"
    type: choice
    options:
      - "< 500 POs/day"
      - "500-5,000 POs/day"
      - "> 5,000 POs/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to ERP)"
      - "outbound (reading from ERP)"
      - "bidirectional sync"
  - key: erp_system
    question: "Which ERP system(s) are involved?"
    type: choice
    options:
      - "SAP S/4HANA"
      - "Oracle ERP Cloud"
      - "NetSuite"
      - "Dynamics 365"
      - "Multi-ERP environment"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/procure-to-pay-integration/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/supplier-management-integration/2026"
      label: "Supplier master data sync — prerequisite for P2P transactional flows"
  related_to:
    - id: "business/erp-integration/ap-automation-integration/2026"
      label: "AP automation — downstream payment processing"
    - id: "business/erp-integration/inventory-receipt-integration/2026"
      label: "Goods receipt integration — feeds 3-way match"
  solves:
    - id: "business/erp-integration/3-way-match-automation/2026"
      label: "Invoice matching automation using P2P data"
  alternative_to:
    - id: "business/erp-integration/source-to-pay-integration/2026"
      label: "Source-to-Pay — broader scope including sourcing and contracts"
  often_confused_with:
    - id: "business/erp-integration/order-to-cash-integration/2026"
      label: "Order-to-Cash (O2C) — sell-side counterpart to P2P"

# === SOURCES ===
sources:
  - id: src1
    title: "What Is Procure-to-Pay?"
    author: IBM
    url: https://www.ibm.com/think/topics/procure-to-pay
    type: technical_blog
    published: 2025-06-15
    reliability: high
  - id: src2
    title: "The Procure-to-Pay (P2P) Process in a Nutshell (9 Steps)"
    author: Kissflow
    url: https://kissflow.com/procurement/procure-to-pay-process-guide/
    type: technical_blog
    published: 2025-08-10
    reliability: moderate_high
  - id: src3
    title: "Purchase Orders Using IDocs, BAPI, or OData API — SAP S/4HANA"
    author: SAP
    url: https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/9905622a5c1f49ba84e9076fc83a9c2c/e69ce957fc00be12e10000000a4450e5.html
    type: official_docs
    published: 2024-10-01
    reliability: authoritative
  - id: src4
    title: "API for Purchase Order — SAP S/4HANA Cloud"
    author: SAP
    url: https://help.sap.com/docs/SAP_S4HANA_CLOUD/0864cb07010642b3bde45a20de4975bc/f7457296d3774e75bc87af7042efa13c.html
    type: official_docs
    published: 2024-10-01
    reliability: authoritative
  - id: src5
    title: "Common Challenges in Implementing Procure-to-Pay Automation"
    author: GEP
    url: https://www.gep.com/blog/technology/common-challenges-in-implementing-procure-to-pay-automation
    type: industry_report
    published: 2025-09-20
    reliability: high
  - id: src6
    title: "REST API for Oracle Fusion Cloud Procurement"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/procurement/23b/fapra/api-suppliers.html
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src7
    title: "Procure-to-Pay Automation: Implementing a Winning P2P Process"
    author: Tipalti
    url: https://tipalti.com/blog/procure-to-pay-automation/
    type: technical_blog
    published: 2025-11-15
    reliability: moderate_high
---

# Procure-to-Pay (P2P) Integration: Requisition to PO to Receipt to Invoice to Payment

## TL;DR

- **Bottom line**: P2P integration connects 5 sequential data objects (requisition, purchase order, goods receipt, invoice, payment) across procurement and finance systems, with 3-way matching (PO + receipt + invoice) as the critical control point that fails most often.
- **Key limit**: 3-way match accuracy depends entirely on receipt timing and cross-system PO number mapping — if either is out of sync, invoices queue up and suppliers escalate.
- **Watch out for**: Master data drift between procurement and finance systems — mismatched supplier IDs, item codes, or UoM conversions cause 60-80% of P2P integration failures.
- **Best for**: Organizations running procurement in a best-of-breed tool (Coupa, SAP Ariba, Jaggaer) while finance/AP lives in the ERP (SAP, Oracle, NetSuite, D365).
- **Authentication**: OAuth 2.0 for cloud ERPs; certificate-based RFC/BAPI for SAP on-premise; token-based authentication (TBA) for NetSuite.

## System Profile

This integration playbook covers the cross-system Procure-to-Pay flow where procurement operations (requisition, sourcing, PO creation) happen in one system and financial operations (invoice matching, payment processing) happen in another — typically the ERP. The most common architecture pairs a cloud procurement platform (Coupa, SAP Ariba, Jaggaer) with a financial ERP (SAP S/4HANA, Oracle ERP Cloud, NetSuite, D365 Finance). This card covers the integration patterns, data mappings, and failure points for all major ERP targets.

For organizations running P2P entirely within a single ERP (e.g., SAP MM/FI or Oracle Procurement Cloud + Payables), the data flow is internal — but the integration patterns still apply when connecting to external suppliers, banks, and tax engines.

| System | Role | API Surface | Direction |
|---|---|---|---|
| Procurement Platform (Coupa, SAP Ariba, Jaggaer) | Source of truth for requisitions, POs, catalogs | REST API, cXML/OCI | Outbound |
| SAP S/4HANA | ERP — financial master, AP, payments | OData v4 (A_PurchaseOrder), BAPI_PO_CREATE1, IDoc ORDERS05 | Inbound |
| Oracle ERP Cloud | ERP — financial master, AP, payments | REST (fscmRestApi/resources), FBDI, SOAP | Inbound |
| NetSuite | ERP — financial master, AP, payments | REST, SuiteTalk SOAP (TransactionBody) | Inbound |
| Dynamics 365 Finance | ERP — financial master, AP, payments | OData v4 (Data Entities), Dataverse API | Inbound |
| iPaaS (MuleSoft, Boomi, Workato, Celigo) | Middleware — orchestration, transformation, error handling | N/A | Orchestrator |

## API Surfaces & Capabilities

| API Surface | System | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|---|
| OData v4 (A_PurchaseOrder) | SAP S/4HANA Cloud | HTTPS/JSON | PO CRUD, real-time sync | 1,000 per page | Fair use / ICM throttling | Yes | No |
| BAPI_PO_CREATE1 / RFC | SAP S/4HANA On-Prem | RFC/ABAP | PO creation from external procurement | 1 PO per call (multi-line) | Dialog work process limit | Yes | No |
| IDoc ORDERS05 | SAP S/4HANA | ALE/EDI | Batch PO transmission to suppliers | 1 PO per IDoc, batch queued | ALE queue processing | No | Yes |
| REST (Procurement) | Oracle ERP Cloud | HTTPS/JSON | PO, requisition, receipt CRUD | 500 per page (default) | Throttled per tenant | Yes | No |
| FBDI (File-Based Data Import) | Oracle ERP Cloud | CSV/REST upload | Bulk PO/invoice import | 250 MB per file | Job queue limited | No | Yes |
| SuiteTalk SOAP | NetSuite | HTTPS/XML | PO, vendor bill, item receipt | 1,000 per search page | 10 concurrent requests | Yes | No |
| REST API | NetSuite | HTTPS/JSON | Real-time PO/invoice operations | 1,000 per page | Token-based concurrency | Yes | No |
| OData v4 (Data Entities) | D365 Finance | HTTPS/JSON | PO, product receipt, vendor invoice | 10,000 per page | 6,000 requests/5 min per user | Yes | Yes (DMF) |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | System | Notes |
|---|---|---|---|
| Max PO line items per API call | 999 | SAP BAPI_PO_CREATE1 | Split POs with >999 lines |
| Max page size (OData) | 1,000 | SAP S/4HANA Cloud | Use $skip/$top for pagination |
| Max page size (REST) | 500 (default), 2,000 (max) | Oracle ERP Cloud | Set via limit parameter |
| Max file size (FBDI) | 250 MB | Oracle ERP Cloud | Split larger files across jobs |
| Max search page size | 1,000 | NetSuite SuiteTalk | Use searchMoreWithId for pagination |
| Max records per page (OData) | 10,000 | D365 Finance | Use $skiptoken for pagination |
| Max payload size | 50 MB | D365 Finance | For batch Data Entity operations |

[src3, src4, src6]

### Rolling / Daily Limits

| Limit Type | Value | System | Notes |
|---|---|---|---|
| OData API calls | Fair use / ICM-managed | SAP S/4HANA Cloud | No hard numeric cap; ICM throttles at high load |
| RFC connections | Dialog work process pool | SAP S/4HANA On-Prem | Limited by configured dialog processes (typically 20-80) |
| REST API requests | Throttled per tenant | Oracle ERP Cloud | Varies by subscription tier; typically 1,000-5,000/min |
| Concurrent FBDI jobs | 4-8 per module | Oracle ERP Cloud | Shared across Procurement + AP |
| Concurrent SuiteTalk | 10 requests | NetSuite | Per-account concurrency |
| REST API governance | 10 units/request | NetSuite | SuiteCloud governance units deducted per operation |
| OData requests | 6,000/5min per user | D365 Finance | Per-user; service-to-service uses a shared pool |
| DMF recurring jobs | 50 concurrent | D365 Finance | Data Management Framework batch imports |

[src3, src4, src6]

## Authentication

| Flow | System | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| OAuth 2.0 SAML Bearer | SAP S/4HANA Cloud | Server-to-server OData calls | Session-based | Via new SAML assertion | Requires Communication Arrangement |
| X.509 Certificate + RFC | SAP S/4HANA On-Prem | Middleware BAPI calls | Session | N/A | SNC (Secure Network Communications) recommended |
| OAuth 2.0 JWT | Oracle ERP Cloud | Server-to-server REST/SOAP | 1 hour | Yes (refresh token) | Requires IDCS/IAM app registration |
| Token-Based Authentication (TBA) | NetSuite | All SuiteTalk/REST calls | Until revoked | N/A | Consumer key + token key/secret |
| OAuth 2.0 Client Credentials | D365 Finance | Server-to-server OData | 1 hour | Yes | Azure AD app registration required |

### Authentication Gotchas

- SAP Communication Arrangements must explicitly list each OData service — a missing service binding returns 403, not 404. [src3]
- Oracle ERP Cloud REST tokens are tenant-scoped — multi-tenant integrations need separate token management per business unit. [src6]
- NetSuite TBA tokens are tied to a specific role — if the role permissions change, the integration silently loses access to records. [src2]
- D365 service-to-service OAuth tokens inherit the Application User's security role — missing data entity permissions return empty result sets, not errors. [src1]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- 3-way match requires all three documents (PO, receipt, invoice) to be present in the AP system before validation can execute — missing receipts block invoice processing entirely.
- Cross-system PO number mapping must be stored in both systems — the procurement system PO number is not the same as the ERP PO number in most implementations.
- Tax determination timing differs by jurisdiction: some require tax at PO creation (India GST), others at invoice posting (US sales tax) — the integration must respect this sequence.
- Tolerance thresholds (price variance %, quantity variance %) must be identical across procurement and AP systems — mismatched tolerances cause invoices to be approved in one system and rejected in the other.
- Supplier master data must synchronize before any PO data flows — an inbound PO referencing a supplier that does not exist in the ERP will fail silently or create an orphan record.
- Currency and UoM (Unit of Measure) conversion must use the same master data source — drift between procurement and ERP conversion tables causes systematic matching failures.

## Integration Pattern Decision Tree

```
START — User needs to integrate P2P across procurement + ERP
├── What's the primary P2P integration scope?
│   ├── Full P2P (Req → PO → Receipt → Invoice → Payment)
│   │   ├── Data volume < 500 POs/day?
│   │   │   ├── YES → Real-time API sync (REST/OData) with middleware
│   │   │   └── NO → Event-driven + batch hybrid (real-time for POs, batch for invoices)
│   │   └── Number of ERP systems?
│   │       ├── Single ERP → Point-to-point via iPaaS (Boomi, Workato)
│   │       └── Multi-ERP → Hub-and-spoke via enterprise iPaaS (MuleSoft, SAP Integration Suite)
│   ├── PO sync only (procurement → ERP)
│   │   ├── SAP target → OData v4 A_PurchaseOrder (Cloud) or BAPI_PO_CREATE1 (On-Prem)
│   │   ├── Oracle target → REST /fscmRestApi/resources/purchaseOrders
│   │   ├── NetSuite target → REST or SuiteTalk PurchaseOrder record
│   │   └── D365 target → OData PurchaseOrderHeadersV2 data entity
│   ├── Invoice matching only (AP automation)
│   │   ├── 2-way match (PO + Invoice) → Simpler, no receipt dependency
│   │   └── 3-way match (PO + Receipt + Invoice) → Requires receipt sync first
│   └── Payment only → Use bank integration, not P2P
├── Which direction?
│   ├── Procurement → ERP (PO push, receipt pull) → Most common
│   ├── ERP → Procurement (catalog sync, budget push) → Supplementary
│   └── Bidirectional → Design conflict resolution for shared objects (supplier, PO status)
├── Middleware or direct?
│   ├── Direct API-to-API → Only for single integration with <100 POs/day
│   ├── iPaaS middleware → Recommended for all production P2P integrations
│   └── File-based (FBDI, IDoc, CSV) → Legacy or ultra-high-volume batch scenarios
└── Error tolerance?
    ├── Zero-loss required → Idempotent PO creation + dead letter queue + reconciliation job
    └── Best-effort → Retry with exponential backoff (not recommended for financial data)
```

## Quick Reference

### P2P Integration Process Flow

| Step | Source System | Action | Target System | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | Procurement | Purchase requisition approved | Procurement | Requisition → PO candidate | Internal workflow; no cross-system integration |
| 2 | Procurement | PO created and approved | ERP (via middleware) | Purchase Order (header + lines) | Retry 3x, then dead letter queue; alert procurement |
| 3 | Procurement | PO transmitted to supplier | Supplier (via EDI/cXML) | PO document (ORDERS05, cXML OrderRequest) | EDI acknowledgment; resend on NACK |
| 4 | Supplier | Order confirmation / ASN | ERP or Procurement | PO acknowledgment, shipping notice | Log; manual follow-up if no response in SLA |
| 5 | Warehouse/ERP | Goods receipt posted | ERP | Goods Receipt / Item Receipt (against PO) | Must link to PO number; orphan receipts flagged |
| 6 | Supplier | Invoice submitted | AP system or ERP | Vendor Invoice (header + lines + tax) | OCR/IDR capture; route to matching queue |
| 7 | ERP (AP module) | 3-way match executed | ERP | PO + Receipt + Invoice comparison | Auto-approve if within tolerance; exception queue if not |
| 8 | ERP (AP module) | Payment run executed | Bank / Payment gateway | Payment batch (ACH, wire, check) | Bank file reconciliation; rejected payments retried |

## Step-by-Step Integration Guide

### 1. Synchronize supplier master data

Before any transactional P2P data flows, ensure supplier/vendor records exist in both procurement and ERP systems with matching cross-reference IDs. [src5]

```python
# Input:  Supplier record from procurement system (Coupa/Ariba)
# Output: Vendor record created/updated in ERP with cross-reference

import requests

def sync_supplier_to_erp(supplier, erp_config):
    """
    Sync a supplier from procurement to ERP.
    Must run BEFORE any PO sync to prevent orphan references.
    """
    # Check if vendor already exists via cross-reference
    existing = requests.get(
        f"{erp_config['base_url']}/vendors",
        params={"external_id": supplier["id"]},
        headers={"Authorization": f"Bearer {erp_config['token']}"}
    )

    if existing.json().get("count", 0) > 0:
        # Update existing vendor
        vendor_id = existing.json()["items"][0]["id"]
        response = requests.patch(
            f"{erp_config['base_url']}/vendors/{vendor_id}",
            json={
                "name": supplier["name"],
                "payment_terms": supplier["payment_terms"],
                "tax_id": supplier["tax_id"],
                "currency": supplier["currency"]
            },
            headers={"Authorization": f"Bearer {erp_config['token']}"}
        )
    else:
        # Create new vendor — include cross-reference ID
        response = requests.post(
            f"{erp_config['base_url']}/vendors",
            json={
                "name": supplier["name"],
                "external_id": supplier["id"],  # Critical: cross-reference
                "payment_terms": supplier["payment_terms"],
                "tax_id": supplier["tax_id"],
                "currency": supplier["currency"]
            },
            headers={"Authorization": f"Bearer {erp_config['token']}"}
        )

    return response.json()
```

**Verify**: Query ERP vendor list filtered by external_id -> expected: vendor record with matching cross-reference.

### 2. Push approved POs from procurement to ERP

When a PO is approved in the procurement system, create the corresponding PO in the ERP. This is the highest-volume integration point. [src3, src4]

```python
# Input:  Approved PO from procurement system
# Output: PO created in ERP with cross-reference PO number

def push_po_to_sap(po, sap_config):
    """
    Create PO in SAP S/4HANA via OData v4 API.
    Idempotent: checks for existing PO by external reference first.
    """
    # Check idempotency — prevent duplicate POs
    check = requests.get(
        f"{sap_config['base_url']}/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001/PurchaseOrder",
        params={"$filter": f"PurchaseOrderType eq 'NB' and YY1_ExtPORef eq '{po['po_number']}'"},
        headers={"Authorization": f"Bearer {sap_config['token']}"}
    )
    if check.json().get("value", []):
        return {"status": "already_exists", "sap_po": check.json()["value"][0]["PurchaseOrder"]}

    # Map procurement PO to SAP PO structure
    sap_po = {
        "CompanyCode": po["company_code"],
        "PurchaseOrderType": "NB",  # Standard PO
        "Supplier": po["vendor_erp_id"],  # Must use ERP vendor ID, not procurement ID
        "PurchasingOrganization": po["purchasing_org"],
        "PurchasingGroup": po["purchasing_group"],
        "DocumentCurrency": po["currency"],
        "YY1_ExtPORef": po["po_number"],  # Cross-reference to procurement PO
        "_PurchaseOrderItem": [
            {
                "PurchaseOrderItem": str((i + 1) * 10).zfill(5),
                "Material": line["material_number"],
                "OrderQuantity": str(line["quantity"]),
                "NetPriceAmount": str(line["unit_price"]),
                "Plant": line["plant"],
                "StorageLocation": line.get("storage_location", ""),
                "AccountAssignmentCategory": line.get("account_category", "")
            }
            for i, line in enumerate(po["lines"])
        ]
    }

    response = requests.post(
        f"{sap_config['base_url']}/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001/PurchaseOrder",
        json=sap_po,
        headers={
            "Authorization": f"Bearer {sap_config['token']}",
            "Content-Type": "application/json",
            "If-Match": "*"
        }
    )
    return response.json()
```

**Verify**: `GET /PurchaseOrder?$filter=YY1_ExtPORef eq '{po_number}'` -> expected: PO with status 'Created'.

### 3. Sync goods receipt to enable 3-way match

When goods are received in the warehouse, post the receipt against the PO in the ERP. This is the prerequisite for 3-way matching. [src1, src2]

```python
# Input:  Goods receipt event (PO number, items received, quantities)
# Output: Goods receipt posted in ERP, linked to PO

def post_goods_receipt_sap(receipt, sap_config):
    """
    Post goods receipt (MIGO equivalent) via SAP OData API.
    Links to PO for 3-way match downstream.
    """
    gr_payload = {
        "GoodsMovementType": "101",  # GR for PO
        "DocumentDate": receipt["receipt_date"],
        "PostingDate": receipt["posting_date"],
        "ReferenceDocument": receipt["delivery_note"],
        "_MaterialDocumentItem": [
            {
                "Material": line["material_number"],
                "Plant": line["plant"],
                "StorageLocation": line["storage_location"],
                "GoodsMovementType": "101",
                "QuantityInEntryUnit": str(line["received_quantity"]),
                "EntryUnit": line["uom"],
                "PurchaseOrder": receipt["erp_po_number"],
                "PurchaseOrderItem": line["po_line_number"]
            }
            for line in receipt["lines"]
        ]
    }

    response = requests.post(
        f"{sap_config['base_url']}/sap/opu/odata4/sap/api_material_document_srv/MaterialDocument",
        json=gr_payload,
        headers={
            "Authorization": f"Bearer {sap_config['token']}",
            "Content-Type": "application/json"
        }
    )
    return response.json()
```

**Verify**: Check material document created with reference to PO number -> expected: GR document with movement type 101.

### 4. Process vendor invoice with 3-way matching

When the supplier invoice arrives, capture it (OCR/EDI/API), match against PO and receipt, and route for approval or exception handling. [src7]

```python
# Input:  Vendor invoice (from OCR, EDI, or supplier portal)
# Output: Invoice posted and matched, or routed to exception queue

def process_invoice_with_matching(invoice, erp_config):
    """
    3-way match: compare invoice against PO and goods receipt.
    Returns match result with variance details.
    """
    # Step 1: Retrieve PO from ERP
    po = requests.get(
        f"{erp_config['base_url']}/purchase-orders/{invoice['po_number']}",
        headers={"Authorization": f"Bearer {erp_config['token']}"}
    ).json()

    # Step 2: Retrieve goods receipts for this PO
    receipts = requests.get(
        f"{erp_config['base_url']}/goods-receipts",
        params={"po_number": invoice["po_number"]},
        headers={"Authorization": f"Bearer {erp_config['token']}"}
    ).json()

    # Step 3: Execute 3-way match per line item
    match_results = []
    for inv_line in invoice["lines"]:
        po_line = next(
            (l for l in po["lines"] if l["item_number"] == inv_line["po_line"]),
            None
        )
        gr_qty = sum(
            r["quantity"] for r in receipts["items"]
            if r["po_line"] == inv_line["po_line"]
        )

        # Price variance check
        price_variance = abs(inv_line["unit_price"] - po_line["unit_price"]) / po_line["unit_price"]
        # Quantity variance check
        qty_variance = abs(inv_line["quantity"] - gr_qty) / gr_qty if gr_qty > 0 else 1.0

        match_results.append({
            "line": inv_line["po_line"],
            "price_match": price_variance <= erp_config["price_tolerance"],  # e.g., 0.02 = 2%
            "qty_match": qty_variance <= erp_config["qty_tolerance"],  # e.g., 0.05 = 5%
            "price_variance_pct": round(price_variance * 100, 2),
            "qty_variance_pct": round(qty_variance * 100, 2)
        })

    all_matched = all(r["price_match"] and r["qty_match"] for r in match_results)

    if all_matched:
        # Auto-post invoice
        return {"status": "auto_approved", "match_results": match_results}
    else:
        # Route to exception queue
        return {"status": "exception", "match_results": match_results}
```

**Verify**: Query invoice status -> expected: either 'auto_approved' or 'exception' with variance percentages.

### 5. Execute payment run

After invoices are approved (matched or manually cleared), execute the payment run to generate payment files for the bank. [src1]

```bash
# SAP: Execute F110 payment run via RFC (typically scheduled, not real-time)
# Oracle: Use Standard Payment Process Request (PPR) via REST API
# NetSuite: Vendor Payment batch via SuiteTalk

# Oracle ERP Cloud — trigger payment process request
curl -X POST "${ORACLE_BASE_URL}/fscmRestApi/resources/11.13.18.05/paymentProcessRequests" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "PaymentProcessProfile": "Standard",
    "PaymentDate": "2026-03-15",
    "PaymentMethod": "EFT",
    "BusinessUnit": "US Operations"
  }'
```

**Verify**: Check payment process request status -> expected: status 'COMPLETED' with payment file generated.

## Code Examples

### Python: End-to-end P2P event handler with middleware pattern

```python
# Input:  Webhook event from procurement system (PO approved, invoice received)
# Output: Corresponding record created in ERP with error handling

import requests
import time
import logging

logger = logging.getLogger("p2p_integration")

class P2PIntegrationHandler:
    """
    Middleware pattern for P2P integration.
    Handles PO push, receipt sync, and invoice matching.
    """

    def __init__(self, procurement_config, erp_config, tolerance_config):
        self.procurement = procurement_config
        self.erp = erp_config
        self.tolerances = tolerance_config  # {"price_pct": 0.02, "qty_pct": 0.05}

    def handle_event(self, event_type, payload):
        handlers = {
            "po_approved": self._handle_po_approved,
            "goods_received": self._handle_goods_received,
            "invoice_received": self._handle_invoice_received,
        }
        handler = handlers.get(event_type)
        if not handler:
            logger.warning(f"Unknown event type: {event_type}")
            return {"status": "ignored"}
        return self._retry_with_backoff(handler, payload, max_retries=3)

    def _retry_with_backoff(self, func, payload, max_retries=3):
        for attempt in range(max_retries):
            try:
                return func(payload)
            except requests.exceptions.HTTPError as e:
                if e.response.status_code == 429:
                    wait = 2 ** attempt
                    logger.info(f"Rate limited, waiting {wait}s")
                    time.sleep(wait)
                elif e.response.status_code >= 500:
                    wait = 2 ** attempt
                    logger.warning(f"Server error, retry {attempt+1}")
                    time.sleep(wait)
                else:
                    raise
        logger.error(f"Max retries exceeded for {func.__name__}")
        return {"status": "dead_letter", "payload": payload}

    def _handle_po_approved(self, po):
        # Validate supplier exists in ERP first
        vendor = self._resolve_vendor(po["supplier_id"])
        if not vendor:
            return {"status": "blocked", "reason": "supplier_not_in_erp"}
        po["vendor_erp_id"] = vendor["id"]
        return self._create_po_in_erp(po)

    def _handle_goods_received(self, receipt):
        return self._post_goods_receipt(receipt)

    def _handle_invoice_received(self, invoice):
        return self._three_way_match(invoice)
```

### JavaScript/Node.js: SAP OData PO creation with CSRF handling

```javascript
// Input:  PO object from procurement system
// Output: SAP PO number or error

const axios = require("axios"); // v1.6+

async function createPOinSAP(po, sapConfig) {
  // SAP OData requires CSRF token for write operations
  const csrfResponse = await axios.get(
    `${sapConfig.baseUrl}/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001/PurchaseOrder`,
    {
      headers: {
        Authorization: `Bearer ${sapConfig.token}`,
        "X-CSRF-Token": "Fetch",
      },
      params: { $top: 0 },
    }
  );
  const csrfToken = csrfResponse.headers["x-csrf-token"];

  // Create PO with CSRF token
  const response = await axios.post(
    `${sapConfig.baseUrl}/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001/PurchaseOrder`,
    {
      CompanyCode: po.companyCode,
      PurchaseOrderType: "NB",
      Supplier: po.vendorErpId,
      DocumentCurrency: po.currency,
      _PurchaseOrderItem: po.lines.map((line, i) => ({
        PurchaseOrderItem: String((i + 1) * 10).padStart(5, "0"),
        Material: line.materialNumber,
        OrderQuantity: String(line.quantity),
        NetPriceAmount: String(line.unitPrice),
        Plant: line.plant,
      })),
    },
    {
      headers: {
        Authorization: `Bearer ${sapConfig.token}`,
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken,
        "If-Match": "*",
      },
    }
  );
  return {
    sapPONumber: response.data.PurchaseOrder,
    status: response.status,
  };
}
```

### cURL: Test 3-way match status in Oracle ERP Cloud

```bash
# Input:  Oracle ERP Cloud credentials, invoice ID
# Output: Invoice match status and variance details

# Step 1: Get invoice details including match status
curl -s -X GET \
  "${ORACLE_BASE_URL}/fscmRestApi/resources/11.13.18.05/invoices/${INVOICE_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" | jq '{
    invoiceNumber: .InvoiceNumber,
    matchStatus: .MatchStatus,
    validationStatus: .ValidationStatus,
    totalAmount: .InvoiceAmount,
    matchAction: .MatchAction
  }'

# Step 2: Check PO matching details for a specific invoice line
curl -s -X GET \
  "${ORACLE_BASE_URL}/fscmRestApi/resources/11.13.18.05/invoices/${INVOICE_ID}/child/invoiceLines" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.items[] | {
    lineNumber: .LineNumber,
    matchType: .MatchType,
    poNumber: .PONumber,
    quantityInvoiced: .Quantity,
    unitPrice: .UnitPrice,
    matchStatus: .MatchStatus
  }'
```

## Data Mapping

### Field Mapping Reference

| Source Field (Procurement) | Target Field (SAP) | Target Field (Oracle) | Target Field (NetSuite) | Type | Gotcha |
|---|---|---|---|---|---|
| po_number | YY1_ExtPORef (custom) | Attribute15 (DFF) | externalId | String | ERP generates its own PO number; store cross-reference in custom field |
| supplier_id | Supplier (LIFNR) | Supplier (VENDOR_ID) | entity (vendor internalId) | Lookup | Must resolve to ERP vendor ID; never pass procurement system ID directly |
| line.item_code | Material (MATNR) | ItemNumber | item (internalId) | Lookup | Item master must be synced; material number formats differ (SAP: 18-char padded) |
| line.quantity | OrderQuantity | Quantity | quantity | Decimal | UoM must match — procurement "EA" vs SAP "ST" vs Oracle "Ea" |
| line.unit_price | NetPriceAmount | Price | rate | Decimal | SAP stores per price unit (e.g., price per 100); divide accordingly |
| currency | DocumentCurrency | CurrencyCode | currency.refName | ISO 4217 | Must match ERP currency master; some ERPs reject 3-letter codes not in their table |
| ship_to_address | Plant + StorageLocation | ShipToLocationId | location (internalId) | Lookup | Address-to-location mapping required; ERP uses location codes, not addresses |
| tax_code | TaxCode (MWSKZ) | TaxClassificationCode | taxCode | Lookup | Tax determination logic differs by ERP and jurisdiction — map carefully |
| payment_terms | PaymentTerms (ZTERM) | PaymentTermsName | terms (internalId) | Lookup | Payment term codes are ERP-specific; "Net 30" in procurement != "ZN30" in SAP |
| account_code | AccountAssignment | DistributionAccount | account (internalId) | Lookup | Chart of accounts mapping required; GL codes differ across systems |

### Data Type Gotchas

- SAP material numbers are 18 characters right-padded with zeros — passing "WIDGET001" fails; must send "000000000WIDGET001". [src3]
- Oracle ERP Cloud REST API returns dates in ISO 8601 format but expects them without timezone offset for some fields — inconsistent across endpoints. [src6]
- NetSuite currency amounts must include the currency record internal ID, not the ISO code — "USD" must be resolved to an internalId first. [src2]
- SAP price per unit calculation: if PriceUnit is 100, a NetPriceAmount of 500.00 means $5.00 per unit, not $500.00 — off-by-100x errors are extremely common. [src3]
- D365 Finance Data Entities expect quantities as strings in some contexts and numbers in others — check the specific entity metadata ($metadata endpoint). [src1]

## Error Handling & Failure Points

### Common Error Codes

| Code / Error | Meaning | System | Resolution |
|---|---|---|---|
| CX_BAPI_ERROR 06 315 | Vendor does not exist in purchasing org | SAP | Sync vendor master with purchasing org assignment before PO creation |
| CX_BAPI_ERROR ME 065 | Material does not exist in plant | SAP | Ensure material master extends to the target plant |
| ORA-20001: PO_CREATION_ERR | PO creation failed validation | Oracle ERP Cloud | Check PO header required fields; usually missing BU or supplier site |
| INVALID_SEARCH_FILTER | Search criteria syntax error | NetSuite | SuiteTalk search syntax differs from REST — check SuiteQL for REST |
| 429 Too Many Requests | Rate limit exceeded | All systems | Exponential backoff: wait 2^n seconds, max 5 retries |
| MATCHING_EXCEPTION | Invoice does not match PO/receipt | All AP systems | Route to exception queue; check variance against tolerance thresholds |
| DUPLICATE_VALUE | PO already exists with this reference | All systems | Idempotency check passed — return existing PO reference |
| CURRENCY_MISMATCH | Invoice currency differs from PO currency | All AP systems | Convert at posting rate or reject — depends on multi-currency config |

[src3, src5, src6]

### Failure Points in Production

- **Supplier master sync lag**: POs arrive in ERP before the supplier record is created. Fix: `Always sync supplier master first; add a pre-check that blocks PO creation if vendor lookup fails.` [src5]
- **Receipt timing gap**: Invoices arrive before goods receipts are posted, causing 3-way match to fail. Fix: `Implement a "park" queue for invoices awaiting receipts; auto-retry match when receipt arrives.` [src7]
- **PO number cross-reference loss**: ERP PO number not written back to procurement system after creation. Fix: `Make the callback (ERP PO number → procurement system) part of the same transaction; if callback fails, log for manual reconciliation.` [src5]
- **UoM conversion errors**: Procurement system uses "Case" (12 units), ERP expects "Each" — quantity multiplied by 12 on one side, not the other. Fix: `Maintain a UoM conversion table in the middleware; validate both sides match before PO creation.` [src3]
- **Tax calculation mismatch**: Procurement calculates tax at PO time, ERP recalculates at invoice posting with different rates. Fix: `Use the ERP's tax engine as the single source of truth; procurement should pass tax-exclusive amounts.` [src1]
- **Tolerance threshold drift**: AP team changes tolerance from 2% to 5% in ERP but procurement system still uses 2%, creating false exceptions. Fix: `Store tolerance thresholds in middleware config, not in individual systems; sync on change.` [src5]

## Anti-Patterns

### Wrong: Passing procurement system supplier ID directly to ERP

```python
# BAD — procurement supplier ID is not the ERP vendor ID
sap_po = {
    "Supplier": procurement_po["supplier_id"],  # "SUP-12345" — does not exist in SAP
    # ...
}
```

### Correct: Resolve supplier cross-reference before PO creation

```python
# GOOD — resolve to ERP vendor ID via cross-reference table
vendor_mapping = get_vendor_crossref(procurement_po["supplier_id"])
sap_po = {
    "Supplier": vendor_mapping["sap_vendor_id"],  # "0000012345" — valid SAP LIFNR
    # ...
}
```

### Wrong: Creating PO without idempotency check

```python
# BAD — retries create duplicate POs
def push_po(po, erp_config):
    return requests.post(f"{erp_config['url']}/purchase-orders", json=po)
    # If this times out and retries, you get 2 POs for 1 procurement PO
```

### Correct: Idempotent PO creation with external reference check

```python
# GOOD — check if PO already exists before creating
def push_po(po, erp_config):
    existing = requests.get(
        f"{erp_config['url']}/purchase-orders",
        params={"external_ref": po["procurement_po_number"]}
    )
    if existing.json().get("count", 0) > 0:
        return existing.json()["items"][0]  # Return existing, don't create duplicate
    return requests.post(f"{erp_config['url']}/purchase-orders", json=po).json()
```

### Wrong: Posting invoice immediately without checking receipt availability

```python
# BAD — 3-way match fails because receipt hasn't been posted yet
def process_invoice(invoice, erp):
    # Invoice arrives at 9:00 AM, receipt not posted until 2:00 PM
    match_result = erp.three_way_match(invoice)  # Always fails for same-day deliveries
    if not match_result["matched"]:
        erp.reject_invoice(invoice)  # Supplier gets rejection, calls AP team
```

### Correct: Park invoice and retry match when receipt arrives

```python
# GOOD — park invoice, subscribe to receipt events, retry match
def process_invoice(invoice, erp, event_bus):
    receipts = erp.get_receipts_for_po(invoice["po_number"])
    if not receipts:
        # Park invoice — don't reject
        erp.park_invoice(invoice, reason="awaiting_receipt")
        # Subscribe to receipt event for this PO
        event_bus.subscribe(
            f"receipt.posted.{invoice['po_number']}",
            lambda receipt: retry_match(invoice, erp)
        )
        return {"status": "parked", "reason": "awaiting_receipt"}
    return erp.three_way_match(invoice)
```

## Common Pitfalls

- **Master data sync ordering**: Running PO sync before supplier/material master sync. Fix: `Enforce dependency ordering: 1) suppliers, 2) materials, 3) POs. Use middleware orchestration to enforce sequence.` [src5]
- **Hardcoded tolerance thresholds**: Embedding 2% price tolerance in integration code instead of reading from configuration. Fix: `Store tolerances in middleware config or ERP-side; read dynamically before each match.` [src5]
- **Ignoring partial receipts**: Assuming 1 PO = 1 receipt. In reality, suppliers ship in multiple batches. Fix: `Aggregate all receipts for a PO line before matching; match against cumulative received quantity, not individual receipt.` [src2]
- **Currency conversion at wrong timestamp**: Converting at PO creation rate instead of invoice posting rate. Fix: `Use the ERP's exchange rate table at invoice posting date; document which rate type (average, spot, contract) the business requires.` [src1]
- **Not handling PO amendments**: Procurement system updates PO quantity/price but ERP copy is stale. Fix: `Implement PO change sync (not just creation); use version numbers or timestamps to detect and propagate changes.` [src3]
- **Single-threaded invoice processing**: Processing all invoices sequentially through matching. Fix: `Parallelize invoice matching per supplier or PO; use message queues (SQS, Service Bus) to distribute load while maintaining per-PO ordering.` [src7]
- **Ignoring ERP-specific price unit conventions**: SAP's "per 100" pricing unit causes 100x price errors. Fix: `Always read PriceUnit from PO line and divide NetPriceAmount accordingly in mapping layer.` [src3]

## Diagnostic Commands

```bash
# Check PO sync status — verify PO exists in SAP with cross-reference
curl -s "${SAP_BASE}/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001/PurchaseOrder?\$filter=YY1_ExtPORef eq 'PROC-PO-12345'" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.value[0] | {PurchaseOrder, PurchaseOrderType, Supplier, CreationDate}'

# Check goods receipt status against PO — verify 3-way match prerequisites
curl -s "${SAP_BASE}/sap/opu/odata4/sap/api_material_document_srv/MaterialDocumentItem?\$filter=PurchaseOrder eq '4500012345'" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.value[] | {MaterialDocument, GoodsMovementType, QuantityInEntryUnit, PostingDate}'

# Oracle: Check invoice match status
curl -s "${ORACLE_BASE}/fscmRestApi/resources/11.13.18.05/invoices?q=InvoiceNumber=${INVOICE_NUM}" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.items[0] | {InvoiceNumber, MatchStatus, ValidationStatus, ApprovalStatus}'

# NetSuite: Verify vendor bill posted against PO
curl -s "${NETSUITE_BASE}/services/rest/record/v1/vendorBill?q=tranId IS ${BILL_NUM}" \
  -H "Authorization: OAuth ..." | jq '.items[0] | {id, tranId, status, total}'

# Check middleware dead letter queue for failed P2P messages
# (MuleSoft example)
curl -s "${MULESOFT_BASE}/api/v1/dlq/messages?queue=p2p-po-sync&status=unprocessed" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.messages | length'
```

## Version History & Compatibility

| ERP System | Current API Version | Previous Version | Breaking Changes | Notes |
|---|---|---|---|---|
| SAP S/4HANA Cloud | 2408 (OData v4) | 2308 | PO API migrated from v2 to v4 OData | BAPI_PO_CREATE1 still supported on-premise |
| Oracle ERP Cloud | 24B (REST) | 23D | Supplier API restructured | FBDI templates updated per release |
| NetSuite | 2024.2 | 2024.1 | REST API expanded PO fields | SuiteTalk SOAP still fully supported |
| D365 Finance | 10.0.40 | 10.0.38 | PurchaseOrderHeadersV2 entity updated | DMF composite entities preferred for bulk |

### Deprecation Policy

SAP follows a minimum 2-year deprecation window for OData APIs; BAPIs have indefinite support on on-premise. Oracle ERP Cloud REST APIs follow the release cadence (quarterly) with minimum 1 year backward compatibility. NetSuite guarantees SuiteTalk backward compatibility for at least 2 major versions. D365 Data Entities are stable within a major version but may change field mappings between releases. [src3, src4, src6]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Procurement runs in a separate system from AP/Finance | All P2P is within a single ERP | Use native ERP P2P workflow (no integration needed) |
| >100 POs/day across procurement + ERP | <10 POs/day with simple matching | Manual process or basic AP automation tool |
| 3-way match is required (regulated industries, large enterprises) | 2-way match sufficient (services-only procurement) | Simplified invoice-to-PO matching |
| Multi-ERP environment with centralized procurement | Single ERP, single entity | Native ERP procurement module |
| Supplier diversity across digital maturity levels | All suppliers on one EDI/portal standard | EDI-only integration |

## Cross-System Comparison

| Capability | SAP S/4HANA | Oracle ERP Cloud | NetSuite | D365 Finance |
|---|---|---|---|---|
| PO Creation API | OData v4 / BAPI / IDoc | REST / FBDI | REST / SuiteTalk | OData Data Entities |
| Goods Receipt API | OData (MaterialDocument) | REST (Receipts) | REST (ItemReceipt) | OData (ProductReceipts) |
| Invoice Matching | Logistics Invoice Verification (MIRO) | Payables Invoice Matching | Vendor Bill Matching | Invoice Matching (AP) |
| 3-Way Match | Built-in (GR-based IV) | Built-in (Match Approval) | Configurable per vendor | Built-in (match policy) |
| Tolerance Config | Per vendor + per material | Per BU + per supplier | Per vendor preference | Per vendor group + matching policy |
| Bulk Import | IDoc queuing, BDC batch | FBDI (CSV upload) | CSV Import, SuiteImport | DMF (Data Management Framework) |
| Event-Driven | Business Events, RAP | Business Events, Oracle Integration Cloud | User Event Scripts, SuiteScript | Business Events, Dataverse |
| Authentication | SAML/X.509/OAuth | OAuth 2.0 JWT | TBA (Token-Based) | Azure AD OAuth 2.0 |
| Price Unit Handling | Per price unit (e.g., per 100) | Per unit (standard) | Per unit (standard) | Per unit (standard) |
| Cross-Ref Field | Custom field (YY1_*) | DFF (Attribute columns) | externalId | Custom field on entity |

## Important Caveats

- 3-way match accuracy is only as good as the receipt posting timeliness — organizations with slow warehouse receipt posting will see high exception rates regardless of integration quality.
- Every ERP handles PO amendments differently — SAP creates change documents, Oracle tracks revisions, NetSuite uses version fields — the integration must handle change propagation for each target.
- Tax determination in P2P is jurisdiction-dependent and can break integrations when expanding to new countries — always test with local tax scenarios before go-live.
- Tolerance thresholds that work for indirect procurement (office supplies, MRO) are too loose for direct materials — consider category-specific tolerance rules.
- This card covers the integration patterns between systems; each ERP's internal P2P configuration (approval workflows, accounting rules, tax setup) must be configured separately.
- API rate limits and concurrency limits vary significantly by ERP edition and subscription tier — always verify against your specific contract and environment.

## Related Units

- [Supplier Management Integration](../../erp-integration/supplier-management-integration/2026) — prerequisite master data sync
- [AP Automation Integration](../../erp-integration/ap-automation-integration/2026) — downstream payment processing
- [3-Way Match Automation](../../erp-integration/3-way-match-automation/2026) — deep dive on matching logic
- [Source-to-Pay Integration](../../erp-integration/source-to-pay-integration/2026) — broader scope including sourcing
- [Order-to-Cash Integration](../../erp-integration/order-to-cash-integration/2026) — sell-side counterpart
