$expand with inline $filter, $orderby, and $select. Agents frequently hallucinate V2 having this capability. [src3, src4]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 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 | SAP Business Accelerator Hub |
| Status | GA (V4 strategic direction; V2 maintenance mode) |
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 | ICM-configured / ~100 rps (cloud) | Yes | Via $batch |
| OData V4 (RAP) | HTTPS/JSON | New development, Fiori elements | ~1,000/page | ICM-configured / ~100 rps (cloud) | Yes | Via $batch |
| SOAP | HTTPS/XML | Legacy middleware (PI/PO), WS-* | Varies by service | Shared with OData (ICM) | Yes | No |
| RFC/BAPI | SAP proprietary | SAP-to-SAP, ABAP-native | 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 |
| 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 |
| Update Method | POST + X-HTTP-METHOD:MERGE | PATCH (partial) or PUT (full) | V4 follows standard HTTP semantics |
| Enum Types | Not supported | Supported | Strongly typed enumerations |
| Actions/Functions | Function Imports only | Actions + Functions (separate) | V4 has clearer separation |
| 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) |
| Max $top value | System-dependent | All queries | On-premise: configurable in 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; 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 PROCTIMEOUT parameter |
| CSRF token validity | Session-scoped | All write operations | Fetch via GET with X-CSRF-Token: Fetch |
| 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 |
| Daily API call quota | No hard daily limit (Cloud: fair use) | 24h | Cloud: throttled under fair-use policy; On-Premise: resource-bound |
| Concurrent HTTP connections | 500 (ICM default) | Per system | On-premise: icm/max_conn; Cloud: managed |
| Max ICM threads | 50 (default) | Per system | On-premise: icm/max_threads; Cloud: auto-scaled |
[src5]
| Limit Type | Per-Transaction Value | Notes |
|---|---|---|
| ICM connection timeout | 60,000 ms (default) | icm/conn_timeout |
| ICM keep-alive timeout | 300s (default) | icm/keep_alive_timeout |
| Dialog work process time | 600s (default) | rdisp/max_wprun_time |
| Max sockets | 2,048 (default) | icm/max_sockets |
| Memory pipe size | 80 MB (default) | mpi/total_size_MB |
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 (Client Credentials) | S/4HANA Cloud server-to-server | ~12h (configurable) | Yes | Recommended for Cloud; uses SAP BTP XSUAA |
| OAuth 2.0 (SAML Bearer) | User-context propagation (Cloud) | Session-scoped | N/A | Principal propagation from IdP |
| SAML 2.0 | SSO, enterprise IdP integration | Session-scoped | N/A | Used with SAP BTP destinations |
| Basic Authentication | On-premise dev/test only | Session timeout (1800s) | N/A | DO NOT use in production Cloud |
| X.509 Client Certificate | High-security on-premise/Cloud | Certificate validity | N/A | Requires PKI infrastructure |
[src5]
X-CSRF-Token: Fetch header, then include in subsequent writes. Tokens are session-scoped. [src1]__next (V2) or @odata.nextLink (V4). There is no client-side override. [src6]$expand depth beyond 3-4 levels causes severe performance degradation and may time out. Design APIs to minimize expansion depth. [src1]If-Match on PATCH/DELETE results in 428 or 412. [src7]START — User needs to integrate with SAP S/4HANA via OData
|-- Which OData version?
| |-- New project (greenfield)? -> OData V4 (RAP-based)
| +-- Existing project? -> Check if V4 equivalent exists on api.sap.com
|-- What's the integration pattern?
| |-- Real-time (individual records, <1s)
| | |-- Single entity CRUD? -> Direct GET/POST/PATCH/DELETE
| | |-- Multiple related entities?
| | | |-- CREATE? -> Deep Insert (V4 preferred)
| | | +-- UPDATE? -> $batch with changeset
| | +-- Need notifications? -> SAP Event Mesh (Cloud) / polling (On-Premise)
| |-- Batch/Bulk (scheduled, high volume)
| | |-- < 1,000 records? -> Simple loop with individual calls
| | |-- 1,000-50,000 records? -> $batch (100-500 ops per batch)
| | +-- > 50,000 records? -> IDoc or file-based (not OData)
| |-- Event-driven -> Business Events (Cloud) / polling with $filter (On-Premise)
| +-- File-based -> Use IDoc/BAPI, not OData
|-- Which direction?
| |-- Inbound -> Verify CSRF token + ETag handling
| |-- Outbound -> Use $select to minimize payload
| +-- Bidirectional -> Conflict resolution via ETags + LastChangedAt
+-- Deployment model?
|-- Cloud -> Communication Arrangement + OAuth 2.0 (XSUAA)
|-- Cloud Private -> Similar to Cloud; custom namespaces allowed
+-- On-Premise -> Basic Auth or X.509; configure ICM for throughput
| Operation | V2 Method | V4 Method | Notes |
|---|---|---|---|
| List entities | GET /{EntitySet} | GET /{EntitySet} | Add $top, $skip, $filter |
| Get by key | GET /{EntitySet}('{key}') | GET /{EntitySet}('{key}') | Single quotes around string keys |
| Create | POST /{EntitySet} | POST /{EntitySet} | CSRF token required |
| Update (partial) | POST + X-HTTP-METHOD: MERGE | PATCH | V2 uses MERGE; V4 uses PATCH |
| Update (full) | PUT | PUT | Replaces entire entity |
| Delete | DELETE | DELETE | ETag in If-Match header |
| Deep insert | POST with nested JSON | POST with navigation | Atomic: all or nothing |
| Batch | POST /$batch (multipart) | POST /$batch (JSON) | Group related ops in changesets |
| Count | GET /$count | GET /$count | Returns plain integer |
| Metadata | GET /$metadata | GET /$metadata | Full EDMX/CSDL schema |
| Mechanism | OData V2 | OData V4 | When to Use |
|---|---|---|---|
| Client-driven | $top=100&$skip=200 | $top=100&$skip=200 | Small datasets, known total count |
| Server-driven | __next property | @odata.nextLink | Large datasets (mandatory) |
| $skiptoken | In __next URL | In nextLink URL | Efficient server-side cursor |
| Inline count | $inlinecount=allpages | $count=true | Get total record count |
[src6]
Browse the SAP Business Accelerator Hub at api.sap.com to find the specific OData service for your business object. [src1, src2]
# 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 — Service: API_BUSINESS_PARTNER
Verify: Search for your business object and confirm API status is "Active" (not Deprecated).
For S/4HANA Cloud, configure a Communication Arrangement and use OAuth 2.0. For on-premise, use Basic Auth or X.509 certificates. [src5]
# S/4HANA Cloud — OAuth 2.0 Client Credentials Flow
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={id}&client_secret={secret}"
# On-Premise — Basic Auth
curl -X GET "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=10" \
-u "{user}:{pass}" -H "Accept: application/json"
Verify: HTTP 200 with d.results (V2) or value (V4) array.
All OData write operations require a CSRF token. Fetch it before any modification request. [src1]
curl -X GET "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=1" \
-H "Authorization: Bearer {token}" \
-H "X-CSRF-Token: Fetch" -H "Accept: application/json" -v 2>&1 | grep csrf
Verify: Response headers contain x-csrf-token: {non-empty-value}.
Follow server-driven pagination by iterating through nextLink URLs until no more pages exist. [src6]
def fetch_all_pages(base_url, headers, params=None):
all_results = []
url = base_url
while url:
response = requests.get(url, headers=headers, params=params)
data = response.json()
if 'value' in data: # V4
all_results.extend(data['value'])
url = data.get('@odata.nextLink')
elif 'd' in data: # V2
all_results.extend(data['d'].get('results', []))
url = data['d'].get('__next')
else:
break
params = {} # nextLink includes all params
return all_results
Verify: len(all_results) matches the $count value.
# 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"}
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'])
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")
// Input: SAP S/4HANA Cloud OData V4 endpoint, OAuth token
// Output: Created sales order with line items
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
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: {
SalesOrderType: "OR", SalesOrganization: "1710",
DistributionChannel: "10", SoldToParty: "10100001",
_Item: [
{ Material: "TG11", RequestedQuantity: "10", RequestedQuantityUnit: "PC" },
{ Material: "TG12", RequestedQuantity: "5", RequestedQuantityUnit: "PC" },
],
},
}
);
console.log(`Created: ${response.data.SalesOrder}`);
# 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/.../$batch" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"requests":[{"id":"1","method":"GET","url":"A_BusinessPartner?$top=5"},{"id":"2","method":"GET","url":"A_BusinessPartnerAddress?$top=5"}]}'
| OData V2 Type | OData V4 Type | SAP ABAP Type | Notes |
|---|---|---|---|
| Edm.String | Edm.String | CHAR, NUMC | Max length from ABAP data element |
| Edm.DateTime | Edm.DateTimeOffset | DATS + TIMS | V2: /Date(ms)/; V4: ISO 8601 |
| 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 to avoid precision loss |
| Edm.Int32 | Edm.Int32 | INT4 | Standard 32-bit integer |
| Edm.Boolean | Edm.Boolean | ABAP_BOOL | true/false |
| Edm.Guid | Edm.Guid | SYSUUID_X16 | UUID format |
Edm.DateTime as /Date(1234567890000)/ (Unix ms). V4 uses ISO 8601 (2026-03-01T00:00:00Z). Middleware must parse both. [src3]Edm.Decimal values are serialized as strings in both V2 and V4. Use a decimal library, not parseFloat(). [src4]Edm.String and must be zero-padded to full length (e.g., "0000001000" for 10-char). [src1]/Date(0)/ (1970-01-01) in V2 — treat as null. [src3]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 400 | Bad Request | Malformed OData URL, invalid $filter, wrong types | Validate query syntax; check EDMX metadata |
| 401 | Unauthorized | Invalid/expired credentials or token | Re-authenticate; verify Communication Arrangement |
| 403 | Forbidden | Missing CSRF token, insufficient auth, no Comm. Arrangement | Fetch CSRF; check roles; verify arrangement |
| 404 | Not Found | Wrong service URL, inactive service | Activate in /IWFND/MAINT_SERVICE (On-Premise) |
| 412 | Precondition Failed | ETag mismatch | Re-read entity, retry with current ETag |
| 428 | Precondition Required | Missing If-Match header | Include If-Match: {etag} header |
| 429 | Too Many Requests | Rate limit exceeded (Cloud) | Exponential backoff; check Retry-After |
| 500 | Internal Server Error | ABAP runtime error, data inconsistency | Check /IWFND/ERROR_LOG; review ABAP dump (ST22) |
__next on the last page if total count is an exact multiple of page size. Fix: Always check if results array is empty before following __next. [src6]Fetch fresh CSRF token immediately before each $batch POST. [src1]JSON.parse() converts Decimal strings to Numbers, losing precision. Fix: Use decimal.js or keep as strings. [src3]Fetch parent first, query children with $filter on parent key. [src1]Check EDMX maxLength and zero-pad. [src1]Verify arrangement in Fiori > Manage Communication Arrangements. [src2]# BAD — $skip forces database to scan and discard rows
for offset in range(0, total_count, page_size):
response = requests.get(f"{base_url}/A_SalesOrder?$skip={offset}&$top={page_size}")
# Page 1000+ takes exponentially longer
# GOOD — $skiptoken uses efficient cursor-based pagination
url = f"{base_url}/A_SalesOrder?$top={page_size}"
while url:
data = requests.get(url, headers=headers).json()
process_page(data['d']['results'])
url = data['d'].get('__next') # Contains $skiptoken
# BAD — N separate HTTP round-trips
for partner in partners:
requests.patch(f"{base_url}/A_BusinessPartner('{partner['id']}')", json=partner['data'])
# GOOD — group updates into $batch (100-500 per batch)
for i in range(0, len(partners), 200):
batch = partners[i:i + 200]
requests.post(f"{base_url}/$batch", data=build_batch(batch))
# 10,000 partners = 50 HTTP round-trips
# BAD — massive response sizes, potential timeout
requests.get(f"{base_url}/A_SalesOrder('1000000')?$expand=to_Item,to_Item/to_PricingElement,to_Partner")
# GOOD — V4: filter and select on expanded entities
requests.get(f"{base_url_v4}/SalesOrder('1000000')?$expand=_Item($select=SalesOrderItem,Material)&$select=SalesOrder,SoldToParty")
/IWFND/MAINT_SERVICE before API calls. [src1]Accept: application/json explicitly. [src3]datetime'2026-01-01T00:00:00'; V4 uses 2026-01-01. Mixing produces 400 errors. [src3, src4]# Check if OData service is active (On-Premise)
curl -s "https://{host}/sap/opu/odata/sap/API_BUSINESS_PARTNER/$metadata" \
-u "{user}:{pass}" -H "Accept: application/xml" | head -5
# Expected: <?xml ...><edmx:Edmx ...>
# 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 csrf
# Expected: x-csrf-token: {token_value}
# Check S/4HANA Cloud API health
curl -s -o /dev/null -w "%{http_code}" \
"https://{tenant}.s4hana.ondemand.com/.../$metadata" -H "Authorization: Bearer {token}"
# Expected: 200 (healthy), 401 (auth), 403 (arrangement), 503 (maintenance)
# ICM parameters (On-Premise — SAP GUI transaction RZ10/RZ11)
# icm/max_conn, icm/max_threads, icm/conn_timeout, PROCTIMEOUT
| S/4HANA Release | Date | Status | Key Changes | Migration Notes |
|---|---|---|---|---|
| 2408 (On-Premise) | 2024-10 | Current | New V4 APIs for manufacturing, warehousing | V2 still supported; V4 recommended |
| 2025 (Private Cloud) | 2025-02 | Current | Service Order V2 deprecated; V4 successor | Migrate Service Order to V4 |
| 2023 (On-Premise) | 2023-10 | Supported | RAP-based V4 APIs mainstream | Minimum for broad V4 coverage |
| 1909 (On-Premise) | 2019-09 | End of Maintenance (2027) | Last pre-RAP release | V2 only; plan migration to 2023+ |
SAP follows a structured API deprecation lifecycle: APIs are marked "Deprecated" on SAP API Business Hub with a successor identified, then enter a minimum 2-year grace period. Starting 2025, all new APIs are OData V4 exclusively. Check SAP Note 2836302 for the latest deprecation schedule. [src2]
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD of business objects | Bulk migration > 50,000 records | IDoc flat files or SAP Data Services |
| Fiori app backend or custom UI5 | Real-time event streaming | SAP Event Mesh + Business Events |
| Structured queries ($filter, $expand) | Unstructured full-text search | SAP Enterprise Search / HANA FTS |
| Standard objects with published APIs | Custom Z-tables without CDS views | Build RAP service first |
| Middleware integration (MuleSoft, Boomi) | Large binary file upload/download | SAP DMS / CMS |
| Cross-system integration (JSON) | SAP-to-SAP in same landscape | RFC/BAPI (lower overhead) |