OData Protocol for SAP — Design and Consumption
Related Skills
sap-rap-comprehensive— Primary way to build OData v4 services in modern ABAPsap-cap-advanced— Primary way to build OData v4 services in Node.js/Java on BTPsap-ui5-fiori— Biggest OData consumer in the SAP ecosystemsap-integration-suite-advanced— OData in iFlows, API Management, and B2B scenariossap-s4hana-extensibility— Released OData APIs vs custom OData services
Quick Start
Check a service's metadata (what it exposes):
GET /sap/opu/odata/sap/ZMY_SERVICE/$metadata # OData v2
GET /odata/v4/my-service/$metadata # OData v4
Query entities with common options:
GET /Products?$filter=Price gt 100 and Category eq 'Electronics'
&$select=ID,Name,Price
&$expand=Supplier($select=Name,Country)
&$orderby=Name asc
&$top=20&$skip=0
&$count=true
Pick v2 or v4:
| Decision | Choose |
|---|---|
| New greenfield backend | v4 |
| Modern ABAP (RAP), CAP | v4 (RAP v4 is default since 7.54) |
| Legacy SEGW in S/4 on-prem | v2 (some SAP standard services are still v2) |
| Fiori Elements on S/4HANA 2023+ | v4 preferred |
| UI5 consumer for legacy backend | v2 (with sap.ui.model.odata.v2.ODataModel) |
Core Concepts
v2 vs v4 — critical differences
| Feature | OData v2 | OData v4 |
|---|---|---|
| Key syntax | /Products('HT-1001') |
/Products(ID='HT-1001') or /Products('HT-1001') |
| Filter on navigation | $filter=Supplier/Name eq 'X' |
$filter=Supplier/Name eq 'X' (same) |
| Count | /$count or $inlinecount=allpages |
$count=true |
| Batch format | multipart/mixed |
application/json |
| Function import | /GetProducts?name='X' |
/getProducts(name='X') |
| Action import | POST /SubmitOrder |
POST /submitOrder |
| Deep insert | Supported | Supported |
$apply |
Not standard | Standard (aggregations) |
| JSON format | Verbose / light | Standard JSON |
| Type system | EDM types | EDM types (extended) |
Entity Data Model (EDM) types
| Type | SQL equivalent | Notes |
|---|---|---|
Edm.String |
VARCHAR/NVARCHAR | MaxLength, Unicode facets |
Edm.Int32 |
INT | 32-bit signed |
Edm.Int64 |
BIGINT | 64-bit signed |
Edm.Decimal |
DECIMAL | Precision, Scale facets |
Edm.Double |
DOUBLE | 64-bit float |
Edm.Boolean |
BOOLEAN | true/false |
Edm.DateTimeOffset |
TIMESTAMP WITH TZ | ISO 8601 with offset |
Edm.Date |
DATE | v4 only (v2 uses DateTime) |
Edm.TimeOfDay |
TIME | v4 only |
Edm.Guid |
UUID | Without hyphens in URLs |
Edm.Binary |
BLOB | Base64 in JSON |
Service document structure
/ # Service document — lists entity sets
/$metadata # EDMX — complete service description
/Products # Entity set
/Products('HT-1001') # Entity
/Products('HT-1001')/Name # Primitive property
/Products('HT-1001')/Supplier # Navigation property
/Products/$count # v4: entity count
Common Patterns
Pattern 1: Query options reference
# Select only specific fields (reduces payload)
?$select=ID,Name,Price
# Include related entities (JOIN)
?$expand=Supplier,Category
?$expand=Supplier($select=Name,Country) # nested select
?$expand=Orders($filter=Status eq 'Open') # nested filter (v4)
# Filter
?$filter=Price gt 100
?$filter=contains(Name, 'Pro') # v4: contains, startswith, endswith
?$filter=substringof('Pro', Name) # v2: substringof
?$filter=Category eq 'Electronics' and Price le 500
?$filter=Date ge 2026-01-01 and Date lt 2026-02-01 # v4 date
?$filter=Supplier/Country eq 'DE' # navigation filter
# Sort
?$orderby=Name asc
?$orderby=Price desc, Name asc
# Pagination
?$top=20&$skip=40 # rows 41-60
# Count
?$count=true # v4
?$inlinecount=allpages # v2
# Search (v4, if service supports Capabilities.SearchRestrictions.Searchable)
?$search=laptop
# Aggregations (v4 with $apply)
?$apply=groupby((Category), aggregate(Price with sum as TotalPrice))
Pattern 2: CRUD operations
# CREATE
POST /Products
Content-Type: application/json
X-CSRF-Token: <token>
{
"ID": "HT-9999",
"Name": "New Product",
"Price": 299.99
}
# READ
GET /Products('HT-1001')
# UPDATE (full replace)
PUT /Products('HT-1001')
Content-Type: application/json
If-Match: W/"etag-value"
{ "ID": "HT-1001", "Name": "Updated", "Price": 199.99 }
# PATCH (partial update) — preferred
PATCH /Products('HT-1001')
Content-Type: application/json
If-Match: W/"etag-value"
{ "Price": 199.99 }
# DELETE
DELETE /Products('HT-1001')
If-Match: W/"etag-value"
Pattern 3: Batch request (v4 JSON batch)
POST /$batch
Content-Type: application/json
{
"requests": [
{ "id": "1", "method": "GET", "url": "Products('HT-1001')" },
{ "id": "2", "method": "GET", "url": "Suppliers('S-001')" },
{
"id": "3",
"method": "PATCH",
"url": "Products('HT-1001')",
"headers": { "Content-Type": "application/json", "If-Match": "*" },
"body": { "Price": 199.99 },
"atomicityGroup": "tx1"
}
]
}
Pattern 4: CSRF token flow (write operations)
# Step 1: Fetch token via any GET
GET /sap/opu/odata/sap/ZMY_SERVICE/
X-CSRF-Token: Fetch
# Response headers include:
X-CSRF-Token: abc123xyz
Set-Cookie: sap-XSRF_...
# Step 2: Use token + cookie in subsequent writes
POST /Products
X-CSRF-Token: abc123xyz
Cookie: sap-XSRF_...
UI5's ODataModel handles this automatically. Custom fetch clients must do it manually.
Pattern 5: CAP OData service
// srv/products-service.cds
using { my.db.Products } from '../db/schema';
service ProductsService @(path: '/products') {
entity Products as projection on my.db.Products;
// Action
action approveProduct(ID: String) returns String;
// Function
function productsOverStock(threshold: Integer) returns array of Products;
}
Exposes GET /products/Products, POST /products/approveProduct, etc. — automatically OData v4.
Pattern 6: RAP projection as OData service
" Service definition (ZSD_PRODUCTS)
@EndUserText.label: 'Products Service'
define service ZSD_PRODUCTS {
expose ZC_PRODUCT as Products;
expose ZC_SUPPLIER as Suppliers;
}
" Service binding (ZSB_PRODUCTS_V4)
" Binding Type: OData V4 - UI
" Service Definition: ZSD_PRODUCTS
Published at /sap/opu/odata4/sap/zsb_products_v4/srvd_a2x/sap/zsd_products/0001/.
Pattern 7: UI5 consumption
// OData v4
const oModel = new sap.ui.model.odata.v4.ODataModel({
serviceUrl: "/odata/v4/products/",
synchronizationMode: "None",
operationMode: "Server",
autoExpandSelect: true
});
// Read
const oContext = oModel.bindContext("/Products('HT-1001')");
oContext.requestObject().then(oProduct => console.log(oProduct));
// Update (PATCH)
oContext.setProperty("Price", 199.99);
await oModel.submitBatch("$auto");
// Create
const oListBinding = oModel.bindList("/Products");
const ID: "HT-9999", Name: "New" });
await oNewContext.created();
Pattern 8: External client (fetch/axios)
// Fetch CSRF token
const tokenRes = await fetch("/sap/opu/odata/sap/ZMY_SERVICE/", {
headers: { "X-CSRF-Token": "Fetch" },
credentials: "include"
});
const csrfToken = tokenRes.headers.get("x-csrf-token");
// Write with token
const createRes = await fetch("/sap/opu/odata/sap/ZMY_SERVICE/Products", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
"Accept": "application/json"
},
body: JSON.stringify({ ID: "HT-9999", Name: "New" })
});
Error Catalog
| HTTP | Error | Cause | Fix |
|---|---|---|---|
| 400 | Invalid filter expression |
Wrong filter syntax or unknown property | Check property name case; eq/ne/gt/lt must be lowercase; strings need single quotes |
| 400 | Could not parse URI |
Invalid key syntax, wrong v2 vs v4 | v2: /Products('HT-1001'); v4: /Products(ID='HT-1001') or /Products('HT-1001') |
| 401 | Authentication failed |
Missing or invalid credentials | Add Basic Auth, form login, or OAuth token |
| 403 | CSRF token validation failed |
Missing CSRF token on write | Fetch token via GET with X-CSRF-Token: Fetch, reuse on POST/PUT/PATCH/DELETE |
| 403 | Not authorized |
User lacks permissions | Check role (PFCG for ABAP, XSUAA scopes for CAP/BTP) |
| 404 | Resource not found |
Wrong URL or service not active | Check service is published; verify path; check /$metadata loads |
| 412 | Precondition Failed |
Missing or stale If-Match etag |
Include If-Match: <etag> from the last read |
| 428 | Precondition Required |
etag required but not sent | Send If-Match: * to force update without etag check (if allowed) |
| 501 | Not Implemented |
Function/feature not supported by service | Check $metadata — does the service declare the capability? |
| 500 | /IWBEP/CX_MGW_BUSI_EXCEPTION |
Business logic error in SEGW data provider | Read the inner message; check backend logs (ST22 for ABAP dumps) |
Performance Tips
- Always
$selectwhat you need — never fetch full entities when you only need 3 columns. $expandis expensive — prefer a second batch request over deep expands.- Server-side paging with
$top+$skip— never load all rows; clients like UI5 do this automatically withgrowing. $count=true— only request the count when the UI actually shows it.- Batch write operations — one
$batchinstead of N single calls reduces round-trip time dramatically. - Use
PATCH, notPUT— PATCH sends only changed fields, PUT sends the whole entity. - Index navigation property columns —
$filteron joined fields otherwise does full scans. $applyfor aggregations (v4) — let the server aggregate instead of fetching raw rows.- Cache
$metadata— it rarely changes; UI5 does this automatically. - Disable cursors for small result sets — OData v4
Capabilities.TopSupportedcan be tuned. - Projection CDS views — at the DB layer, expose only the fields needed by OData.
Gotchas
- Single quotes for strings, no quotes for numbers —
$filter=Name eq 'X'vs$filter=Price eq 100. - Date literals differ — v2:
datetime'2026-01-01T00:00:00'; v4:2026-01-01(Date) or2026-01-01T00:00:00Z(DateTimeOffset). - URL encoding —
$filter=Name eq 'O''Brien'(apostrophe doubled), spaces as%20. $inlinecountvs$count— don't mix up;$inlinecount=allpagesis v2,$count=trueis v4.- Navigation property case sensitivity —
$expand=Supplier≠$expand=supplierin most services. - Action names — v2 uses
GET+ function imports for side-effect-free,POSTotherwise; v4 clearly separates Actions (POST) from Functions (GET). - Deep insert depth — most backends limit to 2–3 levels.
- Batch changesets — atomic unit in v2
$batch; mistakes in one operation roll back the whole changeset. - Lambda operators (
any,all) — v4 only; v2 services ignore or error. - Key predicates with multiple fields —
/Orders(OrderID=5,ItemID=1)— order matters in some implementations.
Debugging Workflow
- Always check
$metadatafirst — it's the source of truth for what's exposed. - Use the Gateway Client (
/IWFND/GW_CLIENT) on ABAP — test requests with full headers/body. - Browser DevTools → Network → XHR filter — see the exact URL, headers, and response.
- Postman/Insomnia — build requests manually, inspect auth headers.
- Enable trace in backend — ABAP:
/IWFND/TRACES; CAP:DEBUG=odata*. - Metadata viewer tools — odata-debugger shows the service structure.
References
references/query-options-cheatsheet.md— All query options with examplesreferences/edm-types-reference.md— EDM type catalog with literals and facetsreferences/odata-v2-vs-v4.md— Side-by-side comparison for migrationtemplates/cap-service-template.cds— CAP OData service skeletontemplates/rap-service-definition.asddls— RAP service definition template
MCP Server Integration
For OData development:
@cap-js/mcp-server— CAP service design, OData metadata, CDS authoringmarianfoo/mcp-sap-docs— SAP documentation lookup (OData SDK, ABAP OData)- Vibing Steampunk MCP — ABAP Gateway service activation, SEGW operations
Install via mcp-configs/fullstack-btp.json.