The SAP C4C OData API, hands on.

SAP Cloud for Customer exposes its CRM data over an OData V2 REST API — 40+ business objects, 1000+ collections. This page lets you build real requests, poke at query options, and walk the write path (CSRF → payload → ETag) without a tenant.

Service root (standard API)
https://myNNNNNN.crm.ondemand.com/sap/c4c/odata/v1/c4codataapi
Service document (list of collections)
/sap/c4c/odata/v1/c4codataapi/← trailing slash required
Metadata ($metadata)
/sap/c4c/odata/v1/c4codataapi/$metadata
Custom services
/sap/c4c/odata/cust/v1/…
Alternative: user-friendly IDs as key
/sap/c4c/odata/uid/v1/c4codataapi/…
01

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.

URL anatomy
HTTP operations
JSON vs Atom
A read request, dissected
https://myNNNNNN.crm.ondemand.com/sap/c4c/odata/v1/c4codataapi/OpportunityCollection('00163E03A0701ED28BCEC7F4AA474109')?$select=OpportunityID,AccountID&$format=json
SegmentMeaning
myNNNNNN.crm.ondemand.comYour C4C tenant host
/sap/c4c/odata/v1/Standard OData services root (/cust/v1/ for custom services built in OData Service Explorer)
c4codataapiThe V2 container service holding all 1000+ collections
OpportunityCollectionEntity set (collection) — the business object list
('00163E…')Key predicate — the ObjectID (or user-friendly ID via the uid variant)
?$select=…&$format=jsonSystem query options, &-separated
Metadata is filterable: $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.
GET
/Collection
Read one entity (with key) or a feed of entities
POST
/Collection
Create an entity. Deep insert = header + children in one POST
PUT
/Collection('id')
Completely replace the entity — full payload required
PATCH
/Collection('id')
Update only the attributes present in the payload
DELETE
/Collection('id')
Delete the record (honors If-Match ETag if sent)
POST
/$batch
Multiple operations in one HTTP call; changesets = transaction boundaries

Reads default to Atom XML. Two ways to get JSON:

GET → JSON via query option
# Read (GET): ask for JSON explicitly
/OpportunityCollection?$format=json

# Write (POST/PATCH/PUT): set the header instead
Content-Type: application/json
typical JSON response envelope (V2)
{
  "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)/"
      }
    ]
  }
}
02

Request Builder

Pick a verb, a collection, and options — the URL assembles live. Copy it into Postman against your tenant (with an Authorization header).





GET
03

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

eq, ge, le, and/or (same-property only for or), startswith/endswith, datetimeoffset
Comma-separated property list
Property + asc/desc
response preview (truncated)
$filter recipes
$search rules
Paging
GoalFilter
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*'
Restrictions: no string/date/math functions beyond startswith/endswith; 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:

BehaviorDetail
Exact term match$search=Example matches “Example 1705 Test”, but $search=Exa does not
Wildcard$search=Exa* matches “Example 1705 Test” and “Examples”
Multiple termsSpace-separated terms are AND; OR is not supported
CombosWorks with $orderby; can combine with $filter (AND semantics)
Not in $batch$search inside a $batch request is not supported
server-side paging (automatic)
# No options? Server pages at 1000 entries and hands you __next:
GET /LeadCollection?$format=json
→ "__next": "…/LeadCollection?$skiptoken=…"   # follow it until absent
client-side paging (you choose the page size)
# How many match?
GET /CorporateAccountCollection/$count?$filter=CountryCode eq 'US'
→ 418

# 4th page of 100:
GET /CorporateAccountCollection?$skip=300&$top=100&$format=json
04

Auth & CSRF

Four ways in. Every request needs an Authorization header — and every modifying request needs a CSRF token too.

MechanismHow
BasicAuthorization: Basic base64(user:password)
OAuth SAML bearerAuthorization: Bearer <token>
SAML frontend SSOSSO tenant URL with odata-sso in the path: https://myXXXXXX-sso.crm.ondemand.com/sap/c4c/odata-sso/c4codataapi
Integration userCommunication 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.

request
GET /sap/c4c/odata/v1/c4codataapi/ HTTP/1.1
x-csrf-token: fetch
Authorization: Basic …

2Read the token from the response

response headers
x-csrf-token: Xi6wOfG-O55Wt8ZkhYW0eA==

3Send it with every POST / PUT / PATCH

modifying request
PATCH /ServiceRequestCollection('00163E0DBD9E1ED596EBDFDA564728AC') HTTP/1.1
x-csrf-token: Xi6wOfG-O55Wt8ZkhYW0eA==
Authorization: Basic …
Content-Type: application/json
Exemption: CSRF handling is not required for OAuth / client-certificate / integration-user server-to-server scenarios.

Useful custom headers

HeaderEffect
odata-no-codedescs: trueSuppress code-description text properties (StatusCodeText etc.) in responses
odata-no-response-payload: trueSuppress the created entity echo after POST
05

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 / deep insert
PATCH vs PUT
$batch
Annotations
create a service request item (simple POST)
POST /ServiceRequestItemCollection HTTP/1.1
x-csrf-token: …
Content-Type: application/json

{
  "Description": "1m water hose",
  "ParentObjectID": "00163E0DBD9E1ED596EBDFDA564728AC",
  "ProductID": "10000060"
}
deep insert — header + child items in one call
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" }
  ]
}
The server echoes the created entity unless you send odata-no-response-payload: true.
PUTPATCH
ScopeReplaces the complete entityUpdates only properties present in payload
Missing propsReset to defaults / clearedUntouched
Use whenYou hold the full object and want overwrite semanticsTargeted field updates — the usual choice
PATCH — change lifecycle status + schedule only
PATCH /ServiceRequestCollection('00163E0DBD9E1ED596EBDFDA564728AC') HTTP/1.1
Content-Type: application/json

{
  "ServiceRequestUserLifeCycleStatusCode": "YJ",
  "ScheduledStartDate": "2015-10-22T00:00:00",
  "ScheduledEndDate":   "2015-10-22T00:00:00"
}
Values sent for ETag and code-description fields (StatusCodeText, CountryCodeText…) are silently ignored on update. And if a property is sap:updatable=false, sending a different value raises an error; sending the same value passes.
one POST, three operations — query + changeset(update + create)
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:

sap:creatable="false"
Don't send on POST — the framework raises an error (unless it's also Nullable="false", like ObjectID: then it's silently ignored)
sap:creatable="true"
Fair game on POST
sap:updatable="false"
Sending a different value on PUT/PATCH → error. Same value → OK
sap:filterable="false"
Using it in $filter → error
Nullable="false" + creatable="true"
Required field — POST without it fails
c4c:context-property
Property needs context: e.g. CountryCode is the context for RegionCode (State)
06

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.

what an ETag looks like (weak, since C4C 1602)
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

conditional update
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.

Delta sync pattern: combine EntityLastChangedOn filtering (§3) with ETag caching to pull only what actually moved since your last run.
07

Watch Out

The limitations that bite people in production — all from SAP's developer guide, all worth designing around up front.

LimitationWorkaround
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 propertyRun each branch as its own query — combine via $batch — and collate
Can't $filter on properties of expanded navigationsFilter the child collection by ParentObjectID instead
$search not allowed in $batchParallel 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/PATCHFetch-then-send flow (§4) — or use integration-user/cert auth where it's waived
Server pages at 1000 entriesFollow __next / $skiptoken, or page manually with $top+$skip and $count
Service document URL needs its trailing slash…just add the slash.
Learn more: SAP Help Portal — C4C OData API · OData API Developer's Guide (GitHub) · OData Service Explorer in your tenant for custom services & sandbox testing.