61 · S/4HANA OData Integration

Import S/4HANA OData metadata, connect via Destination Service, and implement the delegate pattern.

Episode 61 — S/4HANA Integration via OData

What you'll learn

Why this matters

S/4HANA is the core ERP for most enterprises on BTP. CAP apps routinely extend S/4HANA — reading master data, creating documents, subscribing to business events. This is the most common real-world integration pattern in the SAP ecosystem.

Import the API

# Download from SAP API Business Hub (API Hub)
# https://api.sap.com → search "Business Partner" → Download API Specification → EDMX

cds import ./API_BUSINESS_PARTNER.edmx --as cds
# Creates srv/external/API_BUSINESS_PARTNER.cds

Service projection

// srv/procurement-service.cds
using { API_BUSINESS_PARTNER as bp } from './external/API_BUSINESS_PARTNER';

service ProcurementService {
  entity Suppliers as projection on bp.A_BusinessPartner {
    key BusinessPartner   as ID,
        BusinessPartnerFullName as name,
        Country           as country,
        to_BusinessPartnerAddress as addresses
  }
}

S/4HANA Cloud destination (BTP Cockpit)

Name:              S4HANA_CLOUD
Type:              HTTP
URL:               https://<tenant>.s4hana.ondemand.com
Authentication:    OAuth2ClientCredentials
Token Service URL: https://<tenant>.authentication.eu20.hana.ondemand.com/oauth/token
Client ID:         sb-<service-instance>!<id>
Client Secret:     <communication arrangement secret>

Create a Communication Arrangement in S/4HANA for the specific API (e.g., SAP_COM_0008 for Business Partner API), then copy the credentials to the destination.

CAP configuration

// .cdsrc.json
{
  "requires": {
    "API_BUSINESS_PARTNER": {
      "kind":        "odata-v2",
      "model":       "srv/external/API_BUSINESS_PARTNER",
      "[production]": {
        "destination": "S4HANA_CLOUD",
        "path":        "/sap/opu/odata/sap"
      },
      "[development]": {
        "credentials": {
          "url":     "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap",
          "headers": { "APIKey": "YOUR_DEV_KEY" }
        }
      }
    }
  }
}

Delegate pattern (read-through)

// srv/procurement-service.js
const s4 = await cds.connect.to('API_BUSINESS_PARTNER')

// Delegate all READs on Suppliers to S/4HANA
this.on('READ', 'Suppliers', (req) => s4.run(req.query))

CAP translates $filter, $select, $expand, $top, $skip and sends them to S/4HANA.

Mashup: local + S/4HANA data

this.on('READ', 'PurchaseOrders', async (req) => {
  // 1. Read local POs
  const orders = await SELECT.from('PurchaseOrders')

  // 2. Deduplicate supplier IDs
  const supplierIds = [...new Set(orders.map(o => o.supplier_ID))]

  // 3. Fetch supplier names from S/4HANA in one call
  const suppliers = await s4.run(
    SELECT.from('A_BusinessPartner')
      .where({ BusinessPartner: { in: supplierIds } })
      .columns('BusinessPartner', 'BusinessPartnerFullName')
  )

  // 4. Merge
  const map = Object.fromEntries(suppliers.map(s => [s.BusinessPartner, s.BusinessPartnerFullName]))
  return orders.map(o => ({ ...o, supplierName: map[o.supplier_ID] ?? 'Unknown' }))
})

S/4HANA OData V2 quirks

Quirk Workaround
Dates as /Date(timestamp)/ CAP converts automatically with kind: odata-v2
CSRF token required for write CAP fetches and sends X-CSRF-Token automatically
$batch uses V2 format CAP handles automatically
Error in d.error vs error CAP normalises to standard OData error format
Association naming (_to_) Use the generated CDS model — it maps names correctly

Error handling

this.on('createPurchaseOrder', async (req) => {
  try {
    const result = await s4.run(
      INSERT.into('A_PurchaseOrder').entries(req.data)
    )
    return result
  } catch (err) {
    // S/4HANA errors come back as structured OData errors
    if (err.statusCode === 400) {
      return req.reject(400, `S/4HANA rejected the PO: ${err.message}`)
    }
    if (err.statusCode === 403) {
      return req.reject(403, 'Insufficient authorizations in S/4HANA')
    }
    throw err   // unexpected — rethrow for CAP global error handler
  }
})

Hands-on exercise

  1. Download the Business Partner EDMX from SAP API Hub and import it
  2. Create a Suppliers projection and delegate READ to S/4HANA sandbox
  3. Test: GET /procurement/Suppliers?$top=5
  4. Add a mashup: local Orders joined with S/4HANA Supplier names

Checkpoint ✓

Sign in for progress tracking & Q&A →