Calling SAP OData services — URL anatomy, query options, JSON output, sap-client, the CSRF token handshake for writes (and how the SAP Cloud SDK does it), ETags, batch, paging, and what each HTTP failure usually means
How a call to an SAP OData API is shaped and why writes fail. The service URL under /sap/opu/odata/sap/<SERVICE>; V2 query options and $format=json; the sap-client tenant parameter. Writes need an X-CSRF-Token fetched first: the SAP Cloud SDK sends HEAD with X-CSRF-Token fetch (with, then without, a trailing slash), keeps the session cookies, and does this for every non-GET by default. Also covers ETags for updates, $batch, paging, and a symptom table for 401, 403, 404, 405 and 412.
markdownreferencev1E6MH-AWJQstage: calltrust: officialservice: connectivityinterface: odatainterface: restSAP OData URL/sap/opu/odata/sapOData V2OData V4$filter$select$expand$top$skip$format=json$metadatasap-clientX-CSRF-Token fetchCSRF token validation failed403 on POST SAPfetchCsrfToken falseset-cookie with CSRFETag If-Match412 precondition failed$batchpaging __next401 SAP Gateway404 service not foundOData error body
Calling SAP OData services
URL anatomy
https://<host>[:port]/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?$top=5&$select=BusinessPartner,BusinessPartnerFullName&$format=json
\ system / \ Gateway OData root / \ service / \ entity set / \ query options /
- Service and entity names come from the API's page on the Business Accelerator Hub. Fetch
…/<SERVICE>/$metadatato see entity sets, keys and fields. - V2 query options:
$filter,$select,$expand,$orderby,$top,$skipand$inlinecount.$format=jsonreturns JSON with results underd.results. V4 services follow OData V4 (results undervalue). sap-client: pick the tenant (client) on systems with more than one, as a query parameter or header. A destination'ssap-clientproperty makes the SAP Cloud SDK send it.
The hub's reading: the URL root, d.results and the query options are the standard SAP Gateway OData conventions. Family 3 will cite each published API's own specification.
Writes need a CSRF token
SAP Gateway rejects a modifying request (POST, PUT, PATCH, DELETE) that lacks a valid X-CSRF-Token. The handshake, exactly as the SAP Cloud SDK performs it:
HEAD /resource/path/ X-CSRF-Token: fetch → token returned? use it
HEAD /resource/path X-CSRF-Token: fetch → token returned? use it, otherwise send without one
then: POST /resource/path X-CSRF-Token: <token> + the cookies the HEAD set
- Keep the session cookies. The token belongs to the session the
HEADstarted; the SDK reuses itsSet-Cookievalues. A token sent without its cookies fails. - Default behaviour: the SDK fetches a token for every non-GET (
fetchCsrfToken: true), and never for GET. - Turning it off: set
fetchCsrfToken: falseonly for targets that don't use CSRF, such as many non-SAP APIs.
Updates, batches and paging
- ETags: entities with an ETag must be updated with
If-Match: <etag>from the last read. A missing or stale value is rejected (412). $batch: several operations in one multipart request. Writes inside the batch share one CSRF token.- Paging:
- page with
$topand$skip, or follow the server's next link (__nextin V2 JSON,@odata.nextLinkin V4); - never assume one call returns everything.
What failures usually mean
The hub's reading of the usual causes; family 2's error catalogue will add SAP's verbatim messages.
| Status | Usual cause → fix |
|---|---|
| 401 | Wrong user or password, an expired OAuth token, or a communication user not in the arrangement |
| 403 on a write, with a CSRF-related message or header | Missing, expired or session-less CSRF token. Fetch it with X-CSRF-Token: fetch, and send it with the same cookies |
| 403 on a read | The user lacks the role or authorization for that service or object |
| 404 for the whole service | The service isn't activated on the system, or the Cloud Connector doesn't expose that path |
| 405 | The entity set doesn't allow that method. Check the API specification |
| 412 | ETag mismatch. Re-read the entity and resend If-Match |
| 400 with an OData error body | Bad $filter syntax, or a wrong field name or type. Read error.message.value (V2) or error.message (V4) |