---
# === IDENTITY ===
id: business/erp-integration/sap-s4hana-odata-api-capabilities/2026
canonical_question: "What are the SAP S/4HANA OData V2 and V4 API capabilities - pagination, batch, deep insert, rate limits?"
aliases:
  - "SAP S/4HANA OData API features and limitations"
  - "SAP OData V2 vs V4 capabilities comparison for S/4HANA"
  - "SAP S/4HANA API rate limits and pagination"
  - "SAP Gateway OData batch processing and deep insert"
entity_type: erp_integration
domain: business > erp-integration > SAP S/4HANA OData API Capabilities
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "SAP S/4HANA"
    vendor: "SAP"
    version: "2408"
    edition: "Cloud / On-Premise / Private Cloud"
    deployment: "cloud/on-premise/hybrid"
    api_surface: "OData"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.87
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2025 — OData V2 service order APIs deprecated in favor of V4"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Server-driven pagination enforced: no way to force all data into a single response page when backend determines paging is required"
  - "Default page size typically 1,000 records for on-premise (configurable via SAP Gateway); S/4HANA Cloud enforces server-side limits"
  - "OData V2 does not support filtering or sorting on $expand-ed navigation properties — only V4 supports this"
  - "$batch request body has no hard operation count limit but is bounded by ICM request body size (default ~50 MB, configurable)"
  - "S/4HANA Cloud OData request timeout is 10 minutes (600 seconds); on-premise configurable via ICM parameters"
  - "SAP is deprecating OData V2 APIs — all new APIs since 2023 are V4 via RAP framework; existing V2 APIs enter maintenance mode"
  - "Deep update not supported in OData V2; OData V4 supports deep update only in 4.01+ spec (limited RAP support)"
  - "S/4HANA Cloud typical throughput: ~100 requests per second per API endpoint with burst capacity to 200 rps"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs SAP S/4HANA SOAP/RFC/BAPI/IDoc integration"
    use_instead: "business/erp-integration/sap-s4hana-soap-rfc-idoc/2026"
  - condition: "User needs SAP SuccessFactors OData API capabilities"
    use_instead: "business/erp-integration/sap-successfactors-api-capabilities/2026"
  - condition: "User needs SAP BTP Integration Suite middleware setup"
    use_instead: "business/erp-integration/sap-integration-suite-capabilities/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 data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to SAP S/4HANA)"
      - "outbound (reading from SAP S/4HANA)"
      - "bidirectional sync"
  - key: odata_version
    question: "Which OData version does your target API use?"
    type: choice
    options:
      - "OData V2 (legacy/existing APIs)"
      - "OData V4 (new RAP-based APIs)"
      - "Not sure / need both"
  - key: deployment
    question: "What is your SAP S/4HANA deployment model?"
    type: choice
    options:
      - "S/4HANA Cloud (Public Edition)"
      - "S/4HANA Cloud (Private Edition)"
      - "S/4HANA On-Premise"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/sap-s4hana-odata-api-capabilities/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/sap-s4hana-soap-rfc-idoc/2026"
      label: "SAP S/4HANA SOAP, RFC & IDoc Integration"
  solves:
    - id: "business/erp-integration/salesforce-to-sap-s4hana-integration/2026"
      label: "Salesforce to SAP S/4HANA Integration Playbook"
  alternative_to:
    - id: "business/erp-integration/oracle-erp-cloud-rest-api-capabilities/2026"
      label: "Oracle ERP Cloud REST API Capabilities"
  often_confused_with:
    - id: "business/erp-integration/sap-successfactors-api-capabilities/2026"
      label: "SAP SuccessFactors OData API (different system, different limits)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "SAP API Business Hub — S/4HANA OData V2 API Catalog"
    author: SAP
    url: https://api.sap.com/package/S4HANAOPAPI/odata
    type: official_docs
    published: 2025-10-15
    reliability: authoritative
  - id: src2
    title: "SAP API Business Hub — S/4HANA Cloud OData V4 APIs"
    author: SAP
    url: https://api.sap.com/products/SAPS4HANACloud/apis/ODATAV4
    type: official_docs
    published: 2025-10-15
    reliability: authoritative
  - id: src3
    title: "OData API Versions: A Comparative Analysis of V2 and V4"
    author: SAP Community
    url: https://community.sap.com/t5/technology-blog-posts-by-members/odata-api-versions-a-comparative-analysis-of-v2-and-v4/ba-p/13955662
    type: technical_blog
    published: 2024-08-20
    reliability: moderate_high
  - id: src4
    title: "SAP OData V2 vs V4: A Comprehensive Comparison and Best Practices"
    author: Dr. Satyasri Akula
    url: https://medium.com/sap-innovation-hub/sap-odata-v2-vs-v4-a-comprehensive-comparison-and-best-practices-9f1a4f160b5a
    type: technical_blog
    published: 2024-06-15
    reliability: moderate_high
  - id: src5
    title: "Complete Guide to SAP S/4HANA APIs: REST and SOAP Integration"
    author: Apideck
    url: https://www.apideck.com/blog/guide-to-sap-4-hana-rest-and-soap-api
    type: technical_blog
    published: 2024-11-20
    reliability: moderate_high
  - id: src6
    title: "Difference between $skip and $skiptoken in OData"
    author: SAP Community
    url: https://community.sap.com/t5/technology-blog-posts-by-members/difference-between-skip-and-skiptoken-in-odata/ba-p/13560047
    type: community_resource
    published: 2023-09-15
    reliability: moderate
  - id: src7
    title: "SAP Cloud SDK OData V4 Type-safe Client API"
    author: SAP
    url: https://sap.github.io/cloud-sdk/docs/js/features/odata/v4-client
    type: official_docs
    published: 2025-01-10
    reliability: high
---

# SAP S/4HANA OData API Capabilities

## TL;DR

- **Bottom line**: SAP S/4HANA exposes 600+ OData APIs (V2 and V4) for CRUD, batch, and deep operations; all new APIs since 2023 are OData V4 via the RAP framework, while existing V2 APIs enter maintenance mode. [src1, src2]
- **Key limit**: Server-driven pagination is enforced (default ~1,000 records/page on-premise); S/4HANA Cloud limits throughput to ~100 requests/second per endpoint with burst to 200 rps. [src5]
- **Watch out for**: OData V2 cannot filter or sort on expanded navigation properties — only V4 supports `$expand` with inline `$filter`, `$orderby`, and `$select`. Agents frequently hallucinate V2 having this capability. [src3, src4]
- **Best for**: Real-time CRUD of business objects (sales orders, purchase orders, business partners, materials) with structured query capabilities and SAP Fiori element integration. [src1]
- **Authentication**: OAuth 2.0 (S/4HANA Cloud), SAML 2.0 with principal propagation, Basic Auth (on-premise dev/test only), or X.509 client certificates (mTLS) for high-security scenarios. [src5]

## System Profile

SAP S/4HANA is SAP's flagship ERP system, available in three deployment models: Cloud Public Edition (multi-tenant SaaS), Cloud Private Edition (single-tenant managed), and On-Premise (customer-managed). The OData API surface is the primary programmatic interface, built on the SAP Gateway framework (V2) and the ABAP RESTful Application Programming Model (RAP, V4). This card covers OData-specific capabilities across all deployment models. It does NOT cover SOAP, RFC/BAPI, IDoc, or file-based (IDoc-XML) interfaces, which use fundamentally different protocols and have separate rate limits. [src1, src5]

| Property | Value |
|---|---|
| **Vendor** | SAP |
| **System** | SAP S/4HANA 2408 (On-Premise) / 2502 (Cloud) |
| **API Surface** | OData V2 (SAP Gateway) + OData V4 (RAP) |
| **Current Release** | 2408 (On-Premise), continuously updated (Cloud) |
| **Editions Covered** | Cloud Public, Cloud Private, On-Premise |
| **Deployment** | Cloud / On-Premise / Hybrid |
| **API Docs URL** | [SAP Business Accelerator Hub](https://api.sap.com) |
| **Status** | GA (V4 strategic direction; V2 maintenance mode) |

## API Surfaces & Capabilities

SAP S/4HANA organizes APIs into five categories: Application-to-Application (A2A), Business-to-Business (B2B), Application-to-External (A2X), Custom Extension APIs, and Pre-built Business Process APIs. The OData surface handles the vast majority of integration scenarios. [src5]

| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| OData V2 (Gateway) | HTTPS/JSON or Atom/XML | Legacy integrations, existing Fiori apps | ~1,000/page (server-driven) | ICM-configured (on-prem) / ~100 rps (cloud) | Yes | Via $batch |
| OData V4 (RAP) | HTTPS/JSON | New development, Fiori elements, all new APIs | ~1,000/page (server-driven) | ICM-configured (on-prem) / ~100 rps (cloud) | Yes | Via $batch |
| SOAP (Web Services) | HTTPS/XML | Legacy middleware (PI/PO), WS-* requirements | Varies by service | Shared with OData (ICM) | Yes | No |
| RFC/BAPI | SAP proprietary | SAP-to-SAP, ABAP-native integrations | No hard per-call limit | Work process pool | Yes | No |
| IDoc | SAP proprietary/EDI | EDI, async document exchange | 1 document/IDoc | tRFC queue | No (async) | Yes (batched) |

[src1, src2, src5]

### OData V2 vs V4 Feature Comparison

| Feature | OData V2 | OData V4 | Notes |
|---|---|---|---|
| **Default Format** | Atom/XML (JSON optional) | JSON (default) | V4 JSON is lighter, faster to parse |
| **$filter** | Root entity only | Root + expanded entities | V4: `$expand=Items($filter=Amount gt 100)` |
| **$select** | Root entity only | Root + expanded entities | V4: `$expand=Items($select=ItemNo,Amount)` |
| **$orderby** | Root entity only | Root + expanded entities | V4 enables sorted expansion |
| **$count** | `$inlinecount=allpages` | `$count=true` or `/$count` | Different syntax, same purpose |
| **$search** | Not supported | Supported (free-text) | Depends on CDS annotation @Search |
| **Batch ($batch)** | Multipart/mixed | JSON-based batch (preferred) | V4 also supports multipart/mixed |
| **Deep Insert** | CREATE_DEEP_ENTITY | Native via navigation properties | V4 approach is cleaner, atomic |
| **Deep Update** | Not supported | Limited (OData 4.01 draft) | RAP support still maturing |
| **Delta Queries** | Not standard | `$deltatoken` / delta links | Track changes since last query |
| **Lambda Operators** | Not supported | `any()` / `all()` | Filter collections: `Items/any(i:i/Qty gt 5)` |
| **Update Method** | POST + X-HTTP-METHOD:MERGE or PUT | PATCH (partial) or PUT (full) | V4 follows standard HTTP semantics |
| **Enum Types** | Not supported | Supported | Strongly typed enumerations |
| **Containment Navigation** | Not supported | Supported | Parent-child without separate entity set |
| **Actions/Functions** | Function Imports only | Actions (side effects) + Functions (no side effects) | V4 has clearer separation |

[src3, src4, src7]

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per page | ~1,000 (default, configurable) | All OData queries | Server-driven paging via `__next` (V2) or `@odata.nextLink` (V4); cannot be overridden by client |
| Max $top value | System-dependent | All queries | On-premise: configurable in SAP Gateway; Cloud: enforced server-side |
| Max $expand depth | 3-4 levels (typical) | All queries | Deep expansion degrades performance exponentially |
| Max request body size | ~50 MB (ICM default) | $batch, POST, PATCH | On-premise: configurable via `icm/HTTP/max_request_size_KB`; Cloud: fixed |
| Max response size | ~50 MB (ICM default) | All responses | On-premise: configurable; Cloud: fixed |
| Request timeout | 600s (Cloud) / configurable (On-Premise) | All requests | On-premise: `icm/server_port_N PROCTIMEOUT` parameter |
| Max $batch changesets | No hard limit | $batch requests | Bounded by request body size and ICM timeout |
| CSRF token validity | Session-scoped | All write operations | Fetch via GET with `X-CSRF-Token: Fetch` header before POST/PATCH/DELETE |

[src1, src5, src6]

### Rolling / Daily Limits

| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| Request throughput | ~100 rps per API endpoint | Per second | Cloud: ~100 rps baseline, 200 rps burst; On-Premise: ICM thread pool governs |
| Daily API call quota | No hard daily limit (Cloud: fair use) | 24h | Cloud: throttled under fair-use policy; On-Premise: unlimited (resource-bound) |
| Concurrent HTTP connections | 500 (ICM default) | Per system | On-premise: `icm/max_conn`; Cloud: managed by SAP |
| Max ICM threads | 50 (default) | Per system | On-premise: `icm/max_threads`; Cloud: auto-scaled |
| Batch job concurrency | Work-process dependent | Per system | On-premise: limited by dialog/background work processes |

[src5]

### ICM / Gateway Processing Limits (On-Premise)

| Limit Type | Per-Transaction Value | Notes |
|---|---|---|
| ICM connection timeout | 60,000 ms (default) | `icm/conn_timeout` — max time to establish connection |
| ICM keep-alive timeout | 300s (default) | `icm/keep_alive_timeout` — idle connection closure |
| ICM processing timeout | Configurable per service | `icm/server_port_N PROCTIMEOUT=<seconds>` |
| Dialog work process time limit | 600s (default) | `rdisp/max_wprun_time` — max single request processing time |
| Max sockets | 2,048 (default) | `icm/max_sockets` — total socket pool |
| Memory pipe size | 80 MB (default) | `mpi/total_size_MB` — shared memory for ICM-WP communication |

[src5]

## Authentication

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 (Client Credentials) | S/4HANA Cloud server-to-server | Access token: configurable (default 12h) | Yes, via refresh token | Recommended for Cloud integrations; uses SAP BTP XSUAA |
| OAuth 2.0 (SAML Bearer) | User-context propagation (Cloud) | Session-scoped | N/A | Principal propagation from IdP to S/4HANA |
| SAML 2.0 | SSO, enterprise IdP integration | Session-scoped | N/A | Used with SAP BTP destinations |
| Basic Authentication | On-premise dev/test, simple integrations | Session timeout (default 1800s) | N/A | DO NOT use in production Cloud environments |
| X.509 Client Certificate (mTLS) | High-security on-premise/Cloud | Certificate validity period | N/A | Strongest authentication; requires PKI infrastructure |
| SAP Logon Ticket | SAP-to-SAP trust (on-premise) | Configurable (default 8h) | N/A | Limited to trusted SAP landscape |

[src5]

### Authentication Gotchas

- CSRF tokens are mandatory for all write operations (POST, PATCH, PUT, DELETE). Fetch via `GET` to the entity set URL with header `X-CSRF-Token: Fetch`, then include the returned token in subsequent writes. Tokens are session-scoped — if your session expires, you must re-fetch. [src1]
- OAuth 2.0 on S/4HANA Cloud requires SAP BTP as the OAuth authorization server (XSUAA service). You cannot use a third-party OAuth provider directly against S/4HANA Cloud APIs. [src5]
- On-premise Basic Auth credentials are sent base64-encoded (not encrypted) — always use HTTPS. Session cookies (`SAP_SESSIONID_*`) are returned after first auth and should be reused to avoid repeated authentication overhead. [src5]
- S/4HANA Cloud API endpoints require Communication Arrangements configured in the Fiori launchpad. Without the arrangement, the API returns 403 even with valid credentials. [src2]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- Server-driven pagination is non-negotiable: the backend controls page size and clients must follow `__next` (V2) or `@odata.nextLink` (V4) to retrieve subsequent pages. There is no client-side override. [src6]
- OData V2 APIs are in maintenance mode — SAP creates all new APIs exclusively as OData V4 via the RAP framework. Plan for V4 migration. [src2, src3]
- Deep update is not supported in OData V2 and has only limited support in V4 (OData 4.01 spec). For updating parent + child entities, use $batch with individual operations in a changeset. [src3, src4]
- $expand depth beyond 3-4 levels causes severe performance degradation and may time out. Design APIs to minimize expansion depth. [src1]
- S/4HANA Cloud does not allow custom OData V2 services — only RAP-based V4 services can be created in the Cloud extensibility model. [src2]
- Communication Arrangements must be configured in S/4HANA Cloud before any API is accessible — missing arrangements return 403, not 404. [src2]
- ETag-based optimistic locking is enforced on many APIs. Omitting `If-Match` on PATCH/DELETE operations results in 428 (Precondition Required) or 412 (Precondition Failed). [src7]

## Integration Pattern Decision Tree

```
START — User needs to integrate with SAP S/4HANA via OData
|-- Which OData version?
|   |-- New project (greenfield)?
|   |   +-- YES -> OData V4 (RAP-based) — strategic direction
|   |   +-- NO -> Check if your target API has a V4 equivalent on api.sap.com
|   |       |-- V4 available? -> Migrate to V4
|   |       +-- V4 not available? -> Continue with V2 (plan migration)
|-- What's the integration pattern?
|   |-- Real-time (individual records, <1s)
|   |   |-- Single entity CRUD? -> Direct GET/POST/PATCH/DELETE
|   |   |-- Multiple related entities (parent + children)?
|   |   |   |-- CREATE? -> Deep Insert (V4 preferred, V2 via CREATE_DEEP_ENTITY)
|   |   |   +-- UPDATE? -> $batch with changeset (deep update not reliable)
|   |   +-- Need notifications?
|   |       |-- S/4HANA Cloud? -> SAP Event Mesh + Business Events
|   |       +-- On-Premise? -> Enterprise Messaging or custom polling
|   |-- Batch/Bulk (scheduled, high volume)
|   |   |-- < 1,000 records? -> Simple loop with individual OData calls
|   |   |-- 1,000-50,000 records? -> $batch requests (group 100-500 ops per batch)
|   |   |-- > 50,000 records? -> Consider IDoc or file-based (not OData)
|   |   +-- Need transactional consistency? -> $batch changeset (all-or-nothing)
|   |-- Event-driven
|   |   |-- S/4HANA Cloud? -> Business Events via SAP Event Mesh
|   |   +-- On-Premise? -> SAP Gateway event channels or polling with $filter on LastChangedAt
|   +-- File-based
|       +-- Use IDoc/BAPI — OData is not designed for file-based integration
|-- Which direction?
|   |-- Inbound (writing) -> Verify CSRF token flow + ETag handling
|   |-- Outbound (reading) -> Use $select to minimize payload, $filter to reduce records
|   +-- Bidirectional -> Implement conflict resolution via ETags + LastChangedAt timestamps
+-- Deployment model?
    |-- Cloud Public -> Communication Arrangement + OAuth 2.0 (XSUAA)
    |-- Cloud Private -> Similar to Cloud; custom namespaces allowed
    +-- On-Premise -> Basic Auth or X.509; configure ICM parameters for throughput
```

## Quick Reference

### Common OData Operations

| Operation | V2 Method | V2 Endpoint | V4 Method | V4 Endpoint | Notes |
|---|---|---|---|---|---|
| List entities | GET | `/sap/opu/odata/sap/{SERVICE}/{EntitySet}` | GET | `/sap/opu/odata4/sap/{SERVICE}/{EntitySet}` | Add `$top`, `$skip`, `$filter` |
| Get by key | GET | `/{EntitySet}('{key}')` | GET | `/{EntitySet}('{key}')` | Single quotes around string keys |
| Create | POST | `/{EntitySet}` | POST | `/{EntitySet}` | JSON body; CSRF token required |
| Update (partial) | POST + `X-HTTP-METHOD: MERGE` | `/{EntitySet}('{key}')` | PATCH | `/{EntitySet}('{key}')` | V2 uses MERGE; V4 uses PATCH |
| Update (full) | PUT | `/{EntitySet}('{key}')` | PUT | `/{EntitySet}('{key}')` | Replaces entire entity |
| Delete | DELETE | `/{EntitySet}('{key}')` | DELETE | `/{EntitySet}('{key}')` | ETag in If-Match header |
| Deep insert | POST | `/HeaderSet` (with nested JSON) | POST | `/HeaderSet` (with navigation) | Atomic: all or nothing |
| Batch | POST | `/$batch` (multipart/mixed) | POST | `/$batch` (JSON or multipart) | Group related ops in changesets |
| Count | GET | `/{EntitySet}/$count` | GET | `/{EntitySet}/$count` | Returns plain integer |
| Metadata | GET | `/$metadata` | GET | `/$metadata` | Full EDMX/CSDL schema |
| Fetch CSRF | GET | `/{EntitySet}` + header | GET | `/{EntitySet}` + header | `X-CSRF-Token: Fetch` |

[src1, src2, src3]

### Pagination Quick Reference

| Mechanism | OData V2 | OData V4 | When to Use |
|---|---|---|---|
| Client-driven ($top/$skip) | `$top=100&$skip=200` | `$top=100&$skip=200` | Small datasets, known total count |
| Server-driven (nextLink) | `__next` property in response | `@odata.nextLink` in response | Large datasets (mandatory when server enforces) |
| $skiptoken | `$skiptoken=value` (in __next URL) | `$skiptoken=value` (in nextLink URL) | Efficient server-side cursor; do not construct manually |
| Inline count | `$inlinecount=allpages` | `$count=true` | Get total record count with first page |
| Count only | `/$count` (returns integer) | `/$count` (returns integer) | Check result set size before fetching |

[src6]

## Step-by-Step Integration Guide

### 1. Discover and Select the API

Browse the SAP Business Accelerator Hub at `api.sap.com` to find the specific OData service for your business object. Each API listing shows the protocol version (V2 or V4), available entity sets, and required Communication Scenario (for Cloud). [src1, src2]

```bash
# Browse S/4HANA Cloud V4 APIs
# https://api.sap.com/products/SAPS4HANACloud/apis/ODATAV4

# Browse S/4HANA On-Premise V2 APIs
# https://api.sap.com/package/S4HANAOPAPI/odata

# Example: Business Partner API (V2 — A2X)
# Service: API_BUSINESS_PARTNER
# Entity sets: A_BusinessPartner, A_BusinessPartnerAddress, A_Customer, A_Supplier
```

**Verify**: Search for your business object (e.g., "sales order", "business partner") and confirm the API status is "Active" (not Deprecated).

### 2. Authenticate and Obtain Access Token

For S/4HANA Cloud, configure a Communication Arrangement and use OAuth 2.0. For on-premise, use Basic Auth or X.509 certificates. [src5]

```bash
# S/4HANA Cloud — OAuth 2.0 Client Credentials Flow
# Prerequisites: Communication Arrangement + Communication User configured in Fiori launchpad

# Step 1: Get access token from XSUAA
curl -X POST "https://{subdomain}.authentication.{region}.hana.ondemand.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}"

# Response: { "access_token": "eyJ...", "token_type": "bearer", "expires_in": 43199 }

# Step 2: Use token to call S/4HANA API
curl -X GET "https://{tenant}.s4hana.ondemand.com/sap/opu/odata4/sap/api_business_partner/srvd_a2x/sap/businesspartner/0001/A_BusinessPartner?$top=10" \
  -H "Authorization: Bearer {access_token}" \
  -H "Accept: application/json"

# On-Premise — Basic Auth
curl -X GET "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=10" \
  -u "{username}:{password}" \
  -H "Accept: application/json"
```

**Verify**: `HTTP 200` response with `d.results` (V2) or `value` (V4) array containing business partner records.

### 3. Fetch CSRF Token for Write Operations

All OData write operations (POST, PATCH, PUT, DELETE) in SAP require a CSRF token. Fetch it before any modification request. [src1]

```bash
# Fetch CSRF token — GET request to any entity set with X-CSRF-Token: Fetch header
curl -X GET "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=1" \
  -H "Authorization: Bearer {access_token}" \
  -H "X-CSRF-Token: Fetch" \
  -H "Accept: application/json" \
  -v 2>&1 | grep -i "x-csrf-token"

# Response header: x-csrf-token: {token_value}
# Store this token and include in subsequent write requests
# Also store cookies (SAP_SESSIONID_*) for session reuse
```

**Verify**: Response headers contain `x-csrf-token: {non-empty-value}` (not "Required").

### 4. Execute CRUD Operations with Pagination Handling

Handle server-driven pagination by following nextLink URLs until no more pages exist. [src6]

```python
# Input:  SAP OData API URL, authentication credentials
# Output: All records across all pages

import requests

def fetch_all_pages(base_url, headers, params=None):
    """Fetch all pages of an OData result set following server-driven pagination."""
    all_results = []
    url = base_url
    request_params = params or {}

    while url:
        response = requests.get(url, headers=headers, params=request_params)
        response.raise_for_status()
        data = response.json()

        # OData V4: results in 'value', next page in '@odata.nextLink'
        if 'value' in data:
            all_results.extend(data['value'])
            url = data.get('@odata.nextLink')
        # OData V2: results in 'd.results', next page in 'd.__next'
        elif 'd' in data:
            all_results.extend(data['d'].get('results', []))
            url = data['d'].get('__next')
        else:
            break

        # Clear params for subsequent pages — nextLink includes all params
        request_params = {}

    return all_results
```

**Verify**: `len(all_results)` matches the `$count` or `$inlinecount` value from the initial response.

### 5. Execute Batch Requests ($batch)

Group multiple operations into a single HTTP request using $batch. Use changesets for transactional consistency. [src3]

```bash
# OData V2 $batch — multipart/mixed format
# POST to /$batch endpoint with Content-Type: multipart/mixed

curl -X POST "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/$batch" \
  -H "Authorization: Bearer {access_token}" \
  -H "X-CSRF-Token: {csrf_token}" \
  -H "Content-Type: multipart/mixed; boundary=batch_1" \
  --data-binary @- << 'EOF'
--batch_1
Content-Type: multipart/mixed; boundary=changeset_1

--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary

POST A_BusinessPartner HTTP/1.1
Content-Type: application/json

{"BusinessPartnerCategory":"1","BusinessPartnerFullName":"Test Partner 1"}

--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary

POST A_BusinessPartner HTTP/1.1
Content-Type: application/json

{"BusinessPartnerCategory":"1","BusinessPartnerFullName":"Test Partner 2"}

--changeset_1--
--batch_1--
EOF
```

**Verify**: Response `HTTP 202 Accepted` with multipart response body containing `HTTP/1.1 201 Created` for each operation.

### 6. Implement Error Handling and Retries

Handle SAP-specific error responses and implement retry logic for transient failures. [src5]

```python
# Input:  SAP OData API request function
# Output: Resilient API call with retry logic

import time
import requests

def sap_odata_request(method, url, headers, json_body=None, max_retries=3):
    """Execute SAP OData request with retry logic for transient errors."""
    for attempt in range(max_retries):
        try:
            response = requests.request(method, url, headers=headers, json=json_body, timeout=120)

            if response.status_code == 429:
                # Rate limited — back off exponentially
                wait = min(2 ** attempt * 5, 60)
                print(f"Rate limited (429). Waiting {wait}s before retry {attempt + 1}/{max_retries}")
                time.sleep(wait)
                continue

            if response.status_code == 403 and 'CSRF' in response.text:
                # CSRF token expired — re-fetch and retry
                print("CSRF token expired. Re-fetching...")
                headers['X-CSRF-Token'] = fetch_csrf_token(url, headers)
                continue

            if response.status_code in (502, 503, 504):
                # Gateway/service unavailable — transient error
                wait = min(2 ** attempt * 2, 30)
                print(f"Transient error ({response.status_code}). Waiting {wait}s...")
                time.sleep(wait)
                continue

            response.raise_for_status()
            return response

        except requests.exceptions.ConnectionError:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise

    raise Exception(f"Max retries ({max_retries}) exceeded for {method} {url}")
```

**Verify**: Function returns successful response or raises exception after exhausting retries.

## Code Examples

### Python: Read Business Partners with Pagination and $filter

```python
# Input:  SAP S/4HANA OData V2 credentials, filter criteria
# Output: List of business partner records matching criteria

import requests

base_url = "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER"
headers = {
    "Authorization": "Basic {base64_credentials}",
    "Accept": "application/json",
}

# Fetch business partners created after 2025-01-01, selecting specific fields
params = {
    "$filter": "CreationDate gt datetime'2025-01-01T00:00:00'",
    "$select": "BusinessPartner,BusinessPartnerFullName,CreationDate",
    "$top": "500",
    "$inlinecount": "allpages",
}

all_partners = []
response = requests.get(f"{base_url}/A_BusinessPartner", headers=headers, params=params)
data = response.json()

total = int(data['d']['__count'])
all_partners.extend(data['d']['results'])

# Follow server-driven pagination
while '__next' in data['d']:
    response = requests.get(data['d']['__next'], headers=headers)
    data = response.json()
    all_partners.extend(data['d']['results'])

print(f"Fetched {len(all_partners)} of {total} business partners")
```

### JavaScript/Node.js: Deep Insert Sales Order with Items (V4)

```javascript
// Input:  SAP S/4HANA Cloud OData V4 endpoint, OAuth token, order data
// Output: Created sales order with line items

// Using @sap-cloud-sdk/odata-v4 (SAP Cloud SDK)
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");

const salesOrderPayload = {
  SalesOrderType: "OR",
  SalesOrganization: "1710",
  DistributionChannel: "10",
  OrganizationDivision: "00",
  SoldToParty: "10100001",
  // Deep insert: include line items as navigation property
  _Item: [
    {
      Material: "TG11",
      RequestedQuantity: "10",
      RequestedQuantityUnit: "PC",
    },
    {
      Material: "TG12",
      RequestedQuantity: "5",
      RequestedQuantityUnit: "PC",
    },
  ],
};

const response = await executeHttpRequest(
  { destinationName: "S4HANA_CLOUD" },
  {
    method: "POST",
    url: "/sap/opu/odata4/sap/api_salesorder/srvd_a2x/sap/salesorder/0001/SalesOrder",
    headers: { "Content-Type": "application/json" },
    data: salesOrderPayload,
  }
);

console.log(`Created Sales Order: ${response.data.SalesOrder}`);
```

### cURL: $batch Request with Multiple Reads (V4 JSON Format)

```bash
# Input:  OAuth access token, S/4HANA Cloud endpoint
# Output: Multiple entity responses in single HTTP round-trip

curl -X POST "https://{tenant}.s4hana.ondemand.com/sap/opu/odata4/sap/api_business_partner/srvd_a2x/sap/businesspartner/0001/$batch" \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "requests": [
      {
        "id": "1",
        "method": "GET",
        "url": "A_BusinessPartner?$top=5&$select=BusinessPartner,BusinessPartnerFullName"
      },
      {
        "id": "2",
        "method": "GET",
        "url": "A_BusinessPartnerAddress?$top=5&$select=BusinessPartner,AddressID,CityName"
      }
    ]
  }'

# Response: JSON with "responses" array, each containing status + body
```

## Data Mapping

### OData Property Type Reference

| OData V2 Type | OData V4 Type | SAP ABAP Type | Notes |
|---|---|---|---|
| Edm.String | Edm.String | CHAR, NUMC, STRING | Max length from ABAP data element |
| Edm.DateTime | Edm.DateTimeOffset | DATS + TIMS | V2: `/Date(ms)/` format; V4: ISO 8601 `2026-03-01T00:00:00Z` |
| N/A | Edm.Date | DATS | V4 only — date without time |
| N/A | Edm.TimeOfDay | TIMS | V4 only — time without date |
| Edm.Decimal | Edm.Decimal | CURR, QUAN, DEC | String-encoded in V2 to avoid precision loss |
| Edm.Int32 | Edm.Int32 | INT4 | Standard 32-bit integer |
| Edm.Int64 | Edm.Int64 | INT8 | String-encoded in V2 JSON |
| Edm.Boolean | Edm.Boolean | ABAP_BOOL | `true`/`false` |
| Edm.Binary | Edm.Binary | RAW, RAWSTRING | Base64-encoded |
| Edm.Guid | Edm.Guid | SYSUUID_X16 | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format |

[src3, src4]

### Data Type Gotchas

- OData V2 serializes `Edm.DateTime` as `/Date(1234567890000)/` (Unix ms since epoch in JSON). V4 uses ISO 8601 format (`2026-03-01T00:00:00Z`). Middleware that handles both must parse both formats. [src3]
- `Edm.Decimal` values in V2 JSON are serialized as strings (`"1234.56"`) to prevent JavaScript floating-point precision loss. V4 also uses strings for Decimal. Always parse with a decimal library, not `parseFloat()`. [src4]
- SAP NUMC (numeric character) fields appear as `Edm.String` in OData — they must be zero-padded to their full length (e.g., `"0000001000"` for a 10-char NUMC). Sending `"1000"` causes validation errors. [src1]
- Date fields with no value are `null` in V4 but may appear as `/Date(0)/` (1970-01-01) in V2 — treat `/Date(0)/` as null in your mapping logic. [src3]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 400 | Bad Request | Malformed OData URL, invalid $filter syntax, wrong data types | Validate query syntax; check EDMX metadata for correct property names and types |
| 401 | Unauthorized | Invalid/expired credentials or token | Re-authenticate; check token expiry; verify Communication Arrangement (Cloud) |
| 403 | Forbidden / CSRF Required | Missing CSRF token, insufficient authorization, missing Communication Arrangement | Fetch CSRF token; check user roles/authorizations; verify Communication Arrangement exists |
| 404 | Not Found | Wrong service URL, inactive OData service, wrong entity set name | Verify service is activated in `/IWFND/MAINT_SERVICE` (On-Premise) or Communication Arrangement (Cloud) |
| 405 | Method Not Allowed | Using POST on read-only entity set, or DELETE on non-deletable entity | Check entity set annotations for allowed operations (creatable, updatable, deletable) |
| 412 | Precondition Failed | ETag mismatch — entity was modified by another user/process | Re-read the entity to get current ETag, then retry update with new ETag |
| 428 | Precondition Required | Missing If-Match header on PATCH/DELETE | Include `If-Match: {etag}` header (use `*` only for force-overwrite, which skips concurrency check) |
| 429 | Too Many Requests | Rate limit exceeded (Cloud) | Implement exponential backoff; check `Retry-After` header |
| 500 | Internal Server Error | ABAP runtime error, data inconsistency, unhandled exception | Check SAP Gateway error log (transaction `/IWFND/ERROR_LOG`); review ABAP dump (ST22) |
| 501 | Not Implemented | Query option not supported by this specific service | Check service metadata for supported query options |
| 503 | Service Unavailable | System overloaded, maintenance mode | Retry with backoff; check system status |

[src1, src5]

### Failure Points in Production

- **Pagination loop never terminates**: Some OData V2 services return `__next` even on the last page if the total count is an exact multiple of page size. Fix: `Always check if results array is empty before following __next`. [src6]
- **CSRF token expires mid-batch**: Long-running $batch operations can outlive the CSRF token session. Fix: `Fetch a fresh CSRF token immediately before each $batch POST; keep requests under 5 minutes`. [src1]
- **Decimal precision loss in JavaScript**: `JSON.parse()` converts OData Decimal strings to JavaScript Numbers, losing precision beyond 15 significant digits. Fix: `Use a decimal library (e.g., decimal.js) or keep as strings until final presentation`. [src3]
- **$expand performance cliff**: Expanding 3+ levels deep on entities with many records causes exponential response size growth and ICM timeouts. Fix: `Fetch parent first, then query children separately with $filter on parent key`. [src1]
- **Zero-padded NUMC field validation**: Sending "1000" instead of "0000001000" for a 10-digit NUMC field returns a cryptic 400 error with no indication that zero-padding is required. Fix: `Inspect EDMX metadata for maxLength annotation and zero-pad string values to that length`. [src1]
- **Communication Arrangement missing**: API calls to S/4HANA Cloud return 403 even with valid OAuth tokens when the Communication Arrangement is not configured. The error message is generic. Fix: `Verify Communication Arrangement exists in Fiori launchpad > Manage Communication Arrangements for the specific Communication Scenario`. [src2]

## Anti-Patterns

### Wrong: Using $skip/$top for Large Dataset Extraction

```python
# BAD — client-driven pagination with $skip on large tables
# $skip forces the database to scan and discard rows, getting slower with each page
for offset in range(0, total_count, page_size):
    response = requests.get(
        f"{base_url}/A_SalesOrder?$skip={offset}&$top={page_size}",
        headers=headers
    )
    # Page 1000+ takes exponentially longer — $skip=1000000 scans 1M rows first
```

### Correct: Follow Server-Driven Pagination with $skiptoken

```python
# GOOD — follow server-driven nextLink which uses $skiptoken internally
# $skiptoken uses efficient cursor-based pagination at the database level
url = f"{base_url}/A_SalesOrder?$top={page_size}"
while url:
    response = requests.get(url, headers=headers)
    data = response.json()
    process_page(data['d']['results'])
    url = data['d'].get('__next')  # Contains $skiptoken — efficient cursor
```

### Wrong: Individual API Calls in a Loop for Bulk Updates

```python
# BAD — N separate HTTP round-trips for N updates
for partner in partners_to_update:
    response = requests.patch(
        f"{base_url}/A_BusinessPartner('{partner['id']}')",
        headers=headers,
        json=partner['data']
    )
    # 10,000 partners = 10,000 HTTP round-trips + 10,000 CSRF validations
```

### Correct: Use $batch with Changesets for Bulk Operations

```python
# GOOD — group updates into $batch requests (100-500 per batch)
# Single HTTP round-trip, transactional consistency within changeset
batch_size = 200
for i in range(0, len(partners_to_update), batch_size):
    batch = partners_to_update[i:i + batch_size]
    batch_body = build_odata_batch(batch)  # multipart/mixed format
    response = requests.post(
        f"{base_url}/$batch",
        headers={**headers, "Content-Type": "multipart/mixed; boundary=batch_1"},
        data=batch_body
    )
    # 10,000 partners = 50 HTTP round-trips with full transactional control
```

### Wrong: Expanding All Navigation Properties to "Get Everything"

```python
# BAD — expanding everything causes massive response sizes and timeouts
response = requests.get(
    f"{base_url}/A_SalesOrder('1000000')?$expand=to_Item,to_Item/to_PricingElement,to_Partner,to_Note",
    headers=headers
)
# Response can be 10MB+ for a single order with many items — may timeout
```

### Correct: Selective $expand with $select, or Separate Queries

```python
# GOOD — fetch only what you need; use $select on expanded entities (V4)
# OData V4:
response = requests.get(
    f"{base_url_v4}/SalesOrder('1000000')?$expand=_Item($select=SalesOrderItem,Material,OrderQuantity)&$select=SalesOrder,SalesOrderType,SoldToParty",
    headers=headers
)
# Or fetch children separately:
items = requests.get(
    f"{base_url}/A_SalesOrderItem?$filter=SalesOrder eq '1000000'&$select=SalesOrderItem,Material,OrderQuantity",
    headers=headers
)
```

## Common Pitfalls

- **Not activating OData services (On-Premise)**: On-premise S/4HANA ships with most OData services inactive. You must activate them in transaction `/IWFND/MAINT_SERVICE` before they respond to API calls. Without activation, you get 404 or "Service not found". Fix: `Run /IWFND/MAINT_SERVICE, search by technical service name (e.g., API_BUSINESS_PARTNER), and add the service to the system alias`. [src1]
- **Ignoring Content-Type differences between V2 and V4**: OData V2 defaults to Atom/XML; V4 defaults to JSON. Sending `Accept: application/json` is required for V2 JSON. Forgetting this returns XML, which breaks JSON-only parsers. Fix: `Always set Accept: application/json header explicitly for both V2 and V4`. [src3]
- **Using $filter date format incorrectly**: V2 uses `datetime'2026-01-01T00:00:00'` (with `datetime` prefix); V4 uses `2026-01-01` (ISO date). Mixing formats produces 400 errors. Fix: `Check the OData version and use the correct date literal format`. [src3, src4]
- **Hardcoding session timeout values**: SAP session timeouts are configurable by administrators and can vary between systems. Hardcoding a 30-minute timeout in your client code breaks when the admin changes it. Fix: `Always handle 401/403 responses by re-authenticating, regardless of elapsed time`. [src5]
- **Not handling partial success in $batch**: $batch requests outside a changeset can have individual operations succeed or fail independently. Only changeset-wrapped operations are atomic. Fix: `Parse each sub-response in the batch response body; implement retry logic for individually failed operations`. [src3]
- **Using $orderby without index support**: OData `$orderby` on non-indexed fields causes full table scans and timeouts on large tables. Fix: `Check with the SAP Basis team which fields have database indexes; prefer filtering over sorting when possible`. [src1]

## Diagnostic Commands

```bash
# Check if OData service is active (On-Premise — via browser or curl)
curl -s "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/$metadata" \
  -u "{user}:{pass}" -H "Accept: application/xml" | head -5
# Expected: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx ...>

# Check available entity sets for a service
curl -s "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/" \
  -u "{user}:{pass}" -H "Accept: application/json" | python3 -m json.tool
# Expected: JSON with "EntitySets" array listing all available entity sets

# Test CSRF token fetch
curl -v "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=1" \
  -u "{user}:{pass}" -H "X-CSRF-Token: Fetch" -H "Accept: application/json" 2>&1 | grep -i csrf
# Expected: x-csrf-token: {token_value}

# Check ICM parameters (On-Premise — SAP GUI transaction RZ10/RZ11)
# icm/max_conn, icm/max_threads, icm/conn_timeout, icm/server_port_0 PROCTIMEOUT
# No curl equivalent — requires SAP system access

# Monitor Gateway error log (On-Premise — transaction /IWFND/ERROR_LOG)
# Lists all OData errors with timestamps, user, service, and detailed error messages
# No curl equivalent — use SAP GUI

# Check S/4HANA Cloud API health
curl -s -o /dev/null -w "%{http_code}" \
  "https://{tenant}.s4hana.ondemand.com/sap/opu/odata4/sap/api_business_partner/srvd_a2x/sap/businesspartner/0001/$metadata" \
  -H "Authorization: Bearer {token}"
# Expected: 200 (service healthy), 401 (auth issue), 403 (arrangement missing), 503 (maintenance)
```

## Version History & Compatibility

| S/4HANA Release | Date | OData Status | Key API Changes | Migration Notes |
|---|---|---|---|---|
| 2408 (On-Premise) | 2024-10 | Current (On-Premise) | New V4 APIs for manufacturing, warehousing | V2 APIs still supported; V4 recommended for new development |
| 2025 (Private Cloud) | 2025-02 | Current (Private Cloud) | Service Order V2 API deprecated; V4 successor released | Migrate Service Order integrations from V2 to V4 (API_SERVICEORDER) |
| 2502 (Cloud) | 2025-02 | Current (Cloud) | Continuous V4 API additions | Check api.sap.com for latest Cloud APIs |
| 2023 (On-Premise) | 2023-10 | Supported | RAP-based V4 APIs became mainstream | Minimum release for broad V4 API coverage |
| 2021 (On-Premise) | 2021-10 | Extended Maintenance | V4 APIs introduced | Limited V4 coverage; V2 still primary |
| 1909 (On-Premise) | 2019-09 | End of Maintenance (2027) | Last pre-RAP release | V2 only; plan migration to 2023+ for V4 |

[src1, src2]

### Deprecation Policy

SAP follows a structured API deprecation lifecycle: APIs are first marked as "Deprecated" on SAP API Business Hub with a successor API identified, then enter a minimum 2-year grace period before deactivation. Starting with 2025, all new APIs are OData V4 exclusively — V2 receives only critical bug fixes. Check SAP Note 2836302 for the latest deprecation schedule and migration guides. [src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD of business objects (orders, partners, materials) | Bulk data migration > 50,000 records | IDoc flat files or SAP Data Services |
| Fiori app backend or custom UI5 frontend | Real-time event streaming / notifications | SAP Event Mesh + Business Events |
| Structured queries with $filter, $expand, $select | Unstructured full-text search across all entities | SAP Enterprise Search / HANA FTS |
| Standard business objects with published APIs on api.sap.com | Custom Z-tables without CDS views | Build RAP service first, then expose via OData |
| Middleware integration (MuleSoft, Boomi, Workato) | Large binary file upload/download | SAP DMS (Document Management) or CMS |
| Cross-system integration requiring JSON payloads | SAP-to-SAP integration in same landscape | RFC/BAPI (lower overhead, native types) |

## Important Caveats

- S/4HANA Cloud and On-Premise have different API catalogs — not all Cloud APIs exist On-Premise and vice versa. Always verify on api.sap.com for your specific edition. [src1, src2]
- Rate limits and throttling behavior differ significantly between Cloud (SAP-managed, fair-use policy) and On-Premise (customer-configurable ICM parameters). Performance benchmarks from one deployment model do not apply to the other. [src5]
- SAP's OData V2 to V4 migration is ongoing — some business objects only have V2 APIs as of early 2026. Check api.sap.com before assuming V4 availability for your specific entity. [src2]
- Custom OData services built on BW/4HANA, BPC, or other SAP products have different capabilities and limits than S/4HANA OData. This card covers S/4HANA specifically. [src1]
- The throughput figures (~100 rps, burst 200 rps) for S/4HANA Cloud are approximate and subject to fair-use throttling. Actual limits may vary by tenant size, time of day, and SAP's infrastructure capacity. Always implement retry logic with backoff. [src5]

## Related Units

- [SAP S/4HANA SOAP, RFC & IDoc Integration](/business/erp-integration/sap-s4hana-soap-rfc-idoc/2026)
- [Salesforce to SAP S/4HANA Integration Playbook](/business/erp-integration/salesforce-to-sap-s4hana-integration/2026)
- [Oracle ERP Cloud REST API Capabilities](/business/erp-integration/oracle-erp-cloud-rest-api-capabilities/2026)
- [SAP SuccessFactors OData API Capabilities](/business/erp-integration/sap-successfactors-api-capabilities/2026)