Basics
OData V2 over HTTPS. Default payload format is Atom (XML); append $format=json for reads or send Content-Type: application/json for writes. Every response is versioned with ETags for optimistic concurrency.
| Segment | Meaning |
|---|---|
myNNNNNN.crm.ondemand.com | Your C4C tenant host |
/sap/c4c/odata/v1/ | Standard OData services root (/cust/v1/ for custom services built in OData Service Explorer) |
c4codataapi | The V2 container service holding all 1000+ collections |
OpportunityCollection | Entity set (collection) — the business object list |
('00163E…') | Key predicate — the ObjectID (or user-friendly ID via the uid variant) |
?$select=…&$format=json | System query options, &-separated |
$metadata?$filter=ServiceRequest,Opportunity returns only those EntityTypes (plus dependents). Add ?sap-label=true to get human-readable UI labels, localized via the Accept-Language header.Reads default to Atom XML. Two ways to get JSON:
# Read (GET): ask for JSON explicitly /OpportunityCollection?$format=json # Write (POST/PATCH/PUT): set the header instead Content-Type: application/json
{
"d": {
"__count": "39080", // present when $inlinecount=allpages
"results": [
{
"__metadata": { "uri": "…/OpportunityCollection('00163E03…')",
"type": "c4codataapi.Opportunity",
"etag": "W/\"datetimeoffset'2015-…'\"" },
"ObjectID": "00163E03A0701ED28BCEC7F4AA474109",
"OpportunityID": "8000000207",
"ETag": "/Date(1422997623532)/"
}
]
}
}Request Builder
Pick a verb, a collection, and options — the URL assembles live. Copy it into Postman against your tenant (with an Authorization header).
Query Playground
Every system query option C4C supports, with a live example. Toggle options and watch the request — and a realistic response preview — update.
Query options
| Goal | Filter |
|---|---|
| Exact match | $filter=AccountID eq '1000001' |
| Range | $filter=AccountID ge '1000001' and AccountID le '1000020' |
| Since a date (delta sync) | $filter=EntityLastChangedOn ge datetimeoffset'2019-10-01T00:00:00Z' |
| Name prefix | $filter=startswith(AccountName,'Example') |
| Name suffix | $filter=endswith(AccountName,'LLC') |
| Wildcard in value | $filter=UserID eq '*ADMIN*' |
or only works within the same property; no filtering on properties of expanded navigations. For cross-property OR, run separate queries (batch them) and collate client-side.$search (a C4C extension beyond OData V2) matches against properties marked search-relevant — fuzzy-ish full match per term:
| Behavior | Detail |
|---|---|
| Exact term match | $search=Example matches “Example 1705 Test”, but $search=Exa does not |
| Wildcard | $search=Exa* matches “Example 1705 Test” and “Examples” |
| Multiple terms | Space-separated terms are AND; OR is not supported |
| Combos | Works with $orderby; can combine with $filter (AND semantics) |
| Not in $batch | $search inside a $batch request is not supported |
# No options? Server pages at 1000 entries and hands you __next: GET /LeadCollection?$format=json → "__next": "…/LeadCollection?$skiptoken=…" # follow it until absent
# How many match? GET /CorporateAccountCollection/$count?$filter=CountryCode eq 'US' → 418 # 4th page of 100: GET /CorporateAccountCollection?$skip=300&$top=100&$format=json
Auth & CSRF
Four ways in. Every request needs an Authorization header — and every modifying request needs a CSRF token too.
| Mechanism | How |
|---|---|
| Basic | Authorization: Basic base64(user:password) |
| OAuth SAML bearer | Authorization: Bearer <token> |
| SAML frontend SSO | SSO tenant URL with odata-sso in the path: https://myXXXXXX-sso.crm.ondemand.com/sap/c4c/odata-sso/c4codataapi |
| Integration user | Communication system + arrangement with scenario “OData Services for Business Objects”; Basic or certificate auth. Preferred for server-to-server. |
1Fetch a CSRF token
GET the service document with a special header. No state changes — just ask.
GET /sap/c4c/odata/v1/c4codataapi/ HTTP/1.1 x-csrf-token: fetch Authorization: Basic …
2Read the token from the response
x-csrf-token: Xi6wOfG-O55Wt8ZkhYW0eA==3Send it with every POST / PUT / PATCH
PATCH /ServiceRequestCollection('00163E0DBD9E1ED596EBDFDA564728AC') HTTP/1.1 x-csrf-token: Xi6wOfG-O55Wt8ZkhYW0eA== Authorization: Basic … Content-Type: application/json
Useful custom headers
| Header | Effect |
|---|---|
odata-no-codedescs: true | Suppress code-description text properties (StatusCodeText etc.) in responses |
odata-no-response-payload: true | Suppress the created entity echo after POST |
Write Operations
Create, update, delete, deep insert, and $batch with changesets. What the metadata annotations (sap:creatable, sap:updatable, Nullable) let you predict before you send.
POST /ServiceRequestItemCollection HTTP/1.1 x-csrf-token: … Content-Type: application/json { "Description": "1m water hose", "ParentObjectID": "00163E0DBD9E1ED596EBDFDA564728AC", "ProductID": "10000060" }
POST /ServiceRequestCollection HTTP/1.1 Content-Type: application/json { "Name": "Example service enquiry", "ServiceRequestItem": [ // navigation property carries children { "Description": "1m water hose", "ProductID": "10000060", "Quantity": "2" }, { "Description": "Valve adapter", "ProductID": "10000061", "Quantity": "1" } ] }
odata-no-response-payload: true.| PUT | PATCH | |
|---|---|---|
| Scope | Replaces the complete entity | Updates only properties present in payload |
| Missing props | Reset to defaults / cleared | Untouched |
| Use when | You hold the full object and want overwrite semantics | Targeted field updates — the usual choice |
PATCH /ServiceRequestCollection('00163E0DBD9E1ED596EBDFDA564728AC') HTTP/1.1 Content-Type: application/json { "ServiceRequestUserLifeCycleStatusCode": "YJ", "ScheduledStartDate": "2015-10-22T00:00:00", "ScheduledEndDate": "2015-10-22T00:00:00" }
sap:updatable=false, sending a different value raises an error; sending the same value passes.POST /sap/c4c/odata/v1/servicerequest/$batch HTTP/1.1 Content-Type: multipart/mixed; boundary=batch_guid_01 --batch_guid_01 Content-Type: application/http GET ServiceRequestCollection/?$top=3 HTTP/1.1 ← two blank lines after GET operations --batch_guid_01 Content-Type: multipart/mixed; boundary=changeset_guid_01 --changeset_guid_01 Content-Type: application/http Content-ID: 1 PATCH ServiceRequestCollection('00163E0DBD9E1ED596EBDFDA564728AC') HTTP/1.1 Content-Type: application/json { "ServiceRequestUserLifeCycleStatusCode": "YJ" } --changeset_guid_01 Content-Type: application/http Content-ID: 2 POST ServiceRequestItemCollection HTTP/1.1 Content-Type: application/json { "Description": "1m water hose", "ParentObjectID": "00163E0DBD9E1ED596EBDFDA564728AC" } --changeset_guid_01-- --batch_guid_01--
- Changeset = transaction. Operations inside one changeset succeed or fail together.
- Content-ID references let later operations in a changeset use entities created earlier in the same changeset (
$1/…). - No $search inside $batch.
- Query + changeset can mix in one call (as above) — reads are outside the transaction.
$metadata annotates every property. Read these before writing code:
ETags & Concurrency
Every entity carries an ETag (its last-changed datetime). Use it to avoid overwriting someone else's changes — or to skip re-reading unchanged data.
W/"datetimeoffset'2016-04-27T23%3A07%3A31.6809330Z'"1Read → capture ETag
On a single-entity GET it arrives both as the ETag property and in the response header; on feeds, each entry has its own.
2Write with If-Match
PUT /AccountCollection HTTP/1.1 Content-Type: application/json If-Match: W/"datetimeoffset'2015-02-03T21%3A07%3A03.5328420Z'" x-csrf-token: … { "AccountName": "New Name of the account" }
3Outcome
204 No Content — ETag matched, your write landed. 412 Precondition Failed — someone changed the record since your read; re-read, merge, retry.
EntityLastChangedOn filtering (§3) with ETag caching to pull only what actually moved since your last run.Watch Out
The limitations that bite people in production — all from SAP's developer guide, all worth designing around up front.
| Limitation | Workaround |
|---|---|
| No string/date/math functions in $filter (beyond startswith/endswith) | Filter on a wider range client-side, or model a custom OData service / extension field |
or only within the same property | Run each branch as its own query — combine via $batch — and collate |
| Can't $filter on properties of expanded navigations | Filter the child collection by ParentObjectID instead |
| $search not allowed in $batch | Parallel GETs with $search, merged client-side |
| Default format is XML (Atom), not JSON | $format=json on reads; Content-Type: application/json on writes |
| CSRF token needed for POST/PUT/PATCH | Fetch-then-send flow (§4) — or use integration-user/cert auth where it's waived |
| Server pages at 1000 entries | Follow __next / $skiptoken, or page manually with $top+$skip and $count |
| Service document URL needs its trailing slash | …just add the slash. |