Procure-to-Pay (P2P) Integration: Requisition to PO to Receipt to Invoice to Payment

Type: ERP Integration System: SAP S/4HANA, Oracle ERP Cloud, NetSuite, D365 Finance Confidence: 0.85 Sources: 7 Verified: 2026-03-02 Freshness: evolving

TL;DR

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).

SystemRoleAPI SurfaceDirection
Procurement Platform (Coupa, SAP Ariba, Jaggaer)Source of truth for requisitions, POs, catalogsREST API, cXML/OCIOutbound
SAP S/4HANAERP — financial master, AP, paymentsOData v4 (A_PurchaseOrder), BAPI, IDocInbound
Oracle ERP CloudERP — financial master, AP, paymentsREST, FBDI, SOAPInbound
NetSuiteERP — financial master, AP, paymentsREST, SuiteTalk SOAPInbound
Dynamics 365 FinanceERP — financial master, AP, paymentsOData v4, Data EntitiesInbound
iPaaS (MuleSoft, Boomi, Workato, Celigo)Middleware — orchestration, transformationN/AOrchestrator

API Surfaces & Capabilities

API SurfaceSystemProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
OData v4 (A_PurchaseOrder)SAP S/4HANA CloudHTTPS/JSONPO CRUD, real-time sync1,000/pageFair use / ICMYesNo
BAPI_PO_CREATE1 / RFCSAP S/4HANA On-PremRFC/ABAPPO creation from external1 PO (multi-line)Work process limitYesNo
IDoc ORDERS05SAP S/4HANAALE/EDIBatch PO transmission1 PO/IDoc, batch queuedALE queueNoYes
REST (Procurement)Oracle ERP CloudHTTPS/JSONPO, requisition, receipt500/page (default)Throttled/tenantYesNo
FBDIOracle ERP CloudCSV/REST uploadBulk PO/invoice import250 MB/fileJob queueNoYes
SuiteTalk SOAPNetSuiteHTTPS/XMLPO, vendor bill1,000/search10 concurrentYesNo
REST APINetSuiteHTTPS/JSONReal-time PO/invoice1,000/pageToken-basedYesNo
OData v4 (Data Entities)D365 FinanceHTTPS/JSONPO, product receipt10,000/page6,000 req/5minYesYes (DMF)

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueSystemNotes
Max PO line items per API call999SAP BAPI_PO_CREATE1Split POs with >999 lines
Max page size (OData)1,000SAP S/4HANA CloudUse $skip/$top for pagination
Max page size (REST)500 (default), 2,000 (max)Oracle ERP CloudSet via limit parameter
Max file size (FBDI)250 MBOracle ERP CloudSplit larger files across jobs
Max search page size1,000NetSuite SuiteTalkUse searchMoreWithId for pagination
Max records per page10,000D365 FinanceUse $skiptoken for pagination
Max payload size50 MBD365 FinanceFor batch Data Entity operations

Rolling / Daily Limits

Limit TypeValueSystemNotes
OData API callsFair use / ICM-managedSAP S/4HANA CloudNo hard cap; ICM throttles at high load
RFC connectionsDialog work process poolSAP S/4HANA On-PremTypically 20-80 dialog processes
REST API requestsThrottled per tenantOracle ERP Cloud1,000-5,000/min typical
Concurrent FBDI jobs4-8 per moduleOracle ERP CloudShared across Procurement + AP
Concurrent SuiteTalk10 requestsNetSuitePer-account concurrency
REST API governance10 units/requestNetSuiteSuiteCloud governance units
OData requests6,000/5min per userD365 FinanceService-to-service uses shared pool
DMF recurring jobs50 concurrentD365 FinanceData Management Framework batch

Authentication

FlowSystemUse WhenToken LifetimeRefresh?Notes
OAuth 2.0 SAML BearerSAP S/4HANA CloudServer-to-server ODataSession-basedVia new SAML assertionRequires Communication Arrangement
X.509 Certificate + RFCSAP S/4HANA On-PremMiddleware BAPI callsSessionN/ASNC recommended
OAuth 2.0 JWTOracle ERP CloudServer-to-server REST/SOAP1 hourYesIDCS/IAM app registration
Token-Based Auth (TBA)NetSuiteAll SuiteTalk/REST callsUntil revokedN/AConsumer key + token key/secret
OAuth 2.0 Client CredentialsD365 FinanceServer-to-server OData1 hourYesAzure AD app registration

Authentication Gotchas

Constraints

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? → Real-time API sync with middleware
│   │   └── > 500 POs/day? → Event-driven + batch hybrid
│   ├── PO sync only (procurement → ERP)
│   │   ├── SAP target → OData v4 A_PurchaseOrder or BAPI_PO_CREATE1
│   │   ├── Oracle target → REST /fscmRestApi/resources/purchaseOrders
│   │   ├── NetSuite target → REST or SuiteTalk PurchaseOrder
│   │   └── D365 target → OData PurchaseOrderHeadersV2
│   ├── Invoice matching only
│   │   ├── 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
├── Middleware or direct?
│   ├── Direct API-to-API → Only for <100 POs/day
│   ├── iPaaS middleware → Recommended for all production P2P
│   └── File-based (FBDI, IDoc, CSV) → Legacy or ultra-high-volume batch
└── Error tolerance?
    ├── Zero-loss → Idempotent PO creation + dead letter queue + reconciliation
    └── Best-effort → Retry with backoff (not recommended for financial data)

Quick Reference

P2P Integration Process Flow

StepSource SystemActionTarget SystemData ObjectsFailure Handling
1ProcurementPurchase requisition approvedProcurementRequisition → PO candidateInternal workflow
2ProcurementPO created and approvedERP (via middleware)Purchase Order (header + lines)Retry 3x, then DLQ
3ProcurementPO transmitted to supplierSupplier (EDI/cXML)PO documentEDI ACK; resend on NACK
4SupplierOrder confirmation / ASNERP or ProcurementPO acknowledgmentLog; manual follow-up
5Warehouse/ERPGoods receipt postedERPGoods Receipt (against PO)Must link to PO number
6SupplierInvoice submittedAP system or ERPVendor InvoiceOCR/IDR capture; matching queue
7ERP (AP module)3-way match executedERPPO + Receipt + InvoiceAuto-approve or exception queue
8ERP (AP module)Payment run executedBank / Payment gatewayPayment batchBank file reconciliation

Step-by-Step Integration Guide

1. Synchronize supplier master data

Before any transactional P2P data flows, ensure supplier/vendor records exist in both systems with matching cross-reference IDs. [src5]

def sync_supplier_to_erp(supplier, erp_config):
    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:
        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"]}
        )
    else:
        response = requests.post(
            f"{erp_config['base_url']}/vendors",
            json={"name": supplier["name"], "external_id": supplier["id"]}
        )
    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, create the corresponding PO in the ERP via OData/REST. This is the highest-volume integration point. [src3, src4]

def push_po_to_sap(po, sap_config):
    # Idempotency check
    check = requests.get(
        f"{sap_config['base_url']}/.../PurchaseOrder",
        params={"$filter": f"YY1_ExtPORef eq '{po['po_number']}'"}
    )
    if check.json().get("value", []):
        return {"status": "already_exists"}
    # Create PO
    sap_po = {
        "CompanyCode": po["company_code"],
        "PurchaseOrderType": "NB",
        "Supplier": po["vendor_erp_id"],
        "_PurchaseOrderItem": [...]
    }
    return requests.post(..., json=sap_po).json()

Verify: GET /PurchaseOrder?$filter=YY1_ExtPORef eq '{po_number}' → PO with status 'Created'.

3. Sync goods receipt to enable 3-way match

Post goods receipt against the PO in the ERP. This is the prerequisite for 3-way matching. [src1, src2]

def post_goods_receipt_sap(receipt, sap_config):
    gr_payload = {
        "GoodsMovementType": "101",
        "_MaterialDocumentItem": [{
            "PurchaseOrder": receipt["erp_po_number"],
            "QuantityInEntryUnit": str(line["received_quantity"])
        } for line in receipt["lines"]]
    }
    return requests.post(..., json=gr_payload).json()

Verify: Material document created with reference to PO → movement type 101.

4. Process vendor invoice with 3-way matching

Capture invoice, match against PO and receipt, route for approval or exception handling. [src7]

def process_invoice_with_matching(invoice, erp_config):
    po = get_po(invoice["po_number"])
    receipts = get_receipts(invoice["po_number"])
    for inv_line in invoice["lines"]:
        price_variance = abs(inv_line["unit_price"] - po_line["unit_price"]) / po_line["unit_price"]
        qty_variance = abs(inv_line["quantity"] - gr_qty) / gr_qty
        # Auto-approve if within tolerance; exception queue otherwise
    return {"status": "auto_approved" if all_matched else "exception"}

Verify: Invoice status → 'auto_approved' or 'exception' with variance percentages.

5. Execute payment run

After invoices are approved, execute the payment run to generate payment files. [src1]

# Oracle ERP Cloud — trigger payment process request
curl -X POST "${ORACLE_BASE_URL}/fscmRestApi/.../paymentProcessRequests" \
  -H "Authorization: Bearer ${TOKEN}" \
  -d '{"PaymentProcessProfile":"Standard","PaymentDate":"2026-03-15"}'

Verify: Payment process request status → 'COMPLETED'.

Code Examples

Python: End-to-end P2P event handler with middleware pattern

# Input:  Webhook event from procurement system
# Output: Corresponding record created in ERP with error handling

class P2PIntegrationHandler:
    def __init__(self, procurement_config, erp_config, tolerance_config):
        self.procurement = procurement_config
        self.erp = erp_config
        self.tolerances = tolerance_config

    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,
        }
        return self._retry_with_backoff(handlers[event_type], payload)

    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 in (429, 500, 502, 503):
                    time.sleep(2 ** attempt)
                else:
                    raise
        return {"status": "dead_letter", "payload": payload}

JavaScript/Node.js: SAP OData PO creation with CSRF handling

// Input:  PO object from procurement system
// Output: SAP PO number or error

const axios = require("axios"); // v1.6+

async function createPOinSAP(po, sapConfig) {
  // Fetch CSRF token (required for SAP OData write operations)
  const csrf = await axios.get(`${sapConfig.baseUrl}/.../PurchaseOrder`, {
    headers: { Authorization: `Bearer ${sapConfig.token}`, "X-CSRF-Token": "Fetch" },
    params: { $top: 0 }
  });
  const csrfToken = csrf.headers["x-csrf-token"];

  const response = await axios.post(`${sapConfig.baseUrl}/.../PurchaseOrder`, {
    CompanyCode: po.companyCode,
    PurchaseOrderType: "NB",
    Supplier: po.vendorErpId,
    _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: { "X-CSRF-Token": csrfToken, "If-Match": "*" } });

  return { sapPONumber: response.data.PurchaseOrder };
}

cURL: Test 3-way match status in Oracle ERP Cloud

# Input:  Oracle ERP Cloud credentials, invoice ID
# Output: Invoice match status and variance details

curl -s -X GET \
  "${ORACLE_BASE_URL}/fscmRestApi/resources/11.13.18.05/invoices/${INVOICE_ID}" \
  -H "Authorization: Bearer ${TOKEN}" | jq '{
    invoiceNumber: .InvoiceNumber,
    matchStatus: .MatchStatus,
    validationStatus: .ValidationStatus,
    totalAmount: .InvoiceAmount
  }'

Data Mapping

Field Mapping Reference

Source Field (Procurement)SAP TargetOracle TargetNetSuite TargetTypeGotcha
po_numberYY1_ExtPORef (custom)Attribute15 (DFF)externalIdStringERP generates own PO#; store cross-reference
supplier_idSupplier (LIFNR)Supplier (VENDOR_ID)entity (vendor internalId)LookupMust resolve to ERP vendor ID
line.item_codeMaterial (MATNR)ItemNumberitem (internalId)LookupSAP: 18-char zero-padded
line.quantityOrderQuantityQuantityquantityDecimalUoM must match across systems
line.unit_priceNetPriceAmountPricerateDecimalSAP stores per price unit (e.g., per 100)
currencyDocumentCurrencyCurrencyCodecurrency.refNameISO 4217Must match ERP currency master
ship_to_addressPlant + StorageLocationShipToLocationIdlocation (internalId)LookupAddress-to-location mapping required
tax_codeTaxCode (MWSKZ)TaxClassificationCodetaxCodeLookupTax logic differs by ERP + jurisdiction
payment_termsPaymentTerms (ZTERM)PaymentTermsNameterms (internalId)Lookup"Net 30" != "ZN30" in SAP
account_codeAccountAssignmentDistributionAccountaccount (internalId)LookupGL codes differ across systems

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

Code / ErrorMeaningSystemResolution
CX_BAPI_ERROR 06 315Vendor not in purchasing orgSAPSync vendor master with purchasing org
CX_BAPI_ERROR ME 065Material not in plantSAPExtend material master to target plant
ORA-20001: PO_CREATION_ERRPO creation failed validationOracleCheck required fields (BU, supplier site)
INVALID_SEARCH_FILTERSearch syntax errorNetSuiteCheck SuiteTalk vs REST search syntax
429 Too Many RequestsRate limit exceededAllExponential backoff, max 5 retries
MATCHING_EXCEPTIONInvoice doesn't match PO/receiptAll APRoute to exception queue; check tolerances
DUPLICATE_VALUEPO already existsAllReturn existing PO reference (idempotent)
CURRENCY_MISMATCHInvoice currency differs from POAll APConvert at posting rate or reject

Failure Points in Production

Anti-Patterns

Wrong: Passing procurement supplier ID directly to ERP

# 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

# 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 LIFNR

Wrong: Creating PO without idempotency check

# BAD — retries create duplicate POs
def push_po(po, erp):
    return requests.post(f"{erp['url']}/purchase-orders", json=po)
    # Timeout + retry = 2 POs for 1 procurement PO

Correct: Idempotent PO creation with external reference check

# GOOD — check if PO exists before creating
def push_po(po, erp):
    existing = requests.get(f"{erp['url']}/purchase-orders",
        params={"external_ref": po["procurement_po_number"]})
    if existing.json().get("count", 0) > 0:
        return existing.json()["items"][0]
    return requests.post(f"{erp['url']}/purchase-orders", json=po).json()

Wrong: Posting invoice without checking receipt availability

# BAD — 3-way match fails; supplier gets rejection
def process_invoice(invoice, erp):
    match_result = erp.three_way_match(invoice)  # Fails for same-day deliveries
    if not match_result["matched"]:
        erp.reject_invoice(invoice)  # Supplier escalates

Correct: Park invoice and retry match when receipt arrives

# GOOD — park invoice, subscribe to receipt event, retry
def process_invoice(invoice, erp, event_bus):
    receipts = erp.get_receipts_for_po(invoice["po_number"])
    if not receipts:
        erp.park_invoice(invoice, reason="awaiting_receipt")
        event_bus.subscribe(f"receipt.posted.{invoice['po_number']}",
            lambda r: retry_match(invoice, erp))
        return {"status": "parked"}
    return erp.three_way_match(invoice)

Common Pitfalls

Diagnostic Commands

# Check PO sync status in SAP (verify cross-reference)
curl -s "${SAP_BASE}/.../PurchaseOrder?\$filter=YY1_ExtPORef eq 'PROC-PO-12345'" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.value[0] | {PurchaseOrder, Supplier, CreationDate}'

# Check goods receipt status against PO
curl -s "${SAP_BASE}/.../MaterialDocumentItem?\$filter=PurchaseOrder eq '4500012345'" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.value[] | {MaterialDocument, QuantityInEntryUnit}'

# Oracle: Check invoice match status
curl -s "${ORACLE_BASE}/fscmRestApi/.../invoices?q=InvoiceNumber=${INV_NUM}" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.items[0] | {MatchStatus, ValidationStatus}'

# Check middleware dead letter queue
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 SystemCurrent API VersionPrevious VersionBreaking ChangesNotes
SAP S/4HANA Cloud2408 (OData v4)2308PO API migrated v2 to v4 ODataBAPI still supported on-prem
Oracle ERP Cloud24B (REST)23DSupplier API restructuredFBDI templates updated per release
NetSuite2024.22024.1REST API expanded PO fieldsSuiteTalk SOAP fully supported
D365 Finance10.0.4010.0.38PurchaseOrderHeadersV2 updatedDMF composite entities preferred

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Procurement in separate system from AP/FinanceAll P2P within a single ERPNative ERP P2P workflow
>100 POs/day across systems<10 POs/day, simple matchingManual process or basic AP tool
3-way match required (regulated, large enterprise)2-way match sufficient (services only)Simplified invoice-to-PO matching
Multi-ERP with centralized procurementSingle ERP, single entityNative procurement module
Diverse supplier digital maturityAll suppliers on one EDI standardEDI-only integration

Cross-System Comparison

CapabilitySAP S/4HANAOracle ERP CloudNetSuiteD365 Finance
PO Creation APIOData v4 / BAPI / IDocREST / FBDIREST / SuiteTalkOData Data Entities
Goods Receipt APIOData (MaterialDocument)REST (Receipts)REST (ItemReceipt)OData (ProductReceipts)
Invoice MatchingMIRO (Logistics IV)Payables Invoice MatchingVendor Bill MatchingInvoice Matching (AP)
3-Way MatchBuilt-in (GR-based IV)Built-in (Match Approval)Configurable per vendorBuilt-in (match policy)
Tolerance ConfigPer vendor + per materialPer BU + per supplierPer vendor preferencePer vendor group + policy
Bulk ImportIDoc queuing, BDC batchFBDI (CSV upload)CSV Import, SuiteImportDMF
Event-DrivenBusiness Events, RAPBusiness Events, OICUser Event ScriptsBusiness Events, Dataverse
AuthenticationSAML/X.509/OAuthOAuth 2.0 JWTTBAAzure AD OAuth 2.0
Price Unit HandlingPer price unit (e.g., per 100)Per unit (standard)Per unit (standard)Per unit (standard)
Cross-Ref FieldCustom field (YY1_*)DFF (Attribute columns)externalIdCustom field on entity

Important Caveats

Related Units