We'd like to share an important change to the Usage Records API: As of October 27, 2026, the new OData API (v2) is available. The existing RESTful API (v1) was officially deprecated on November 9, 2026 and will be decommissioned on November 9, 2027.
This blog explains the advantages of v2 and provides detailed instructions on migrating from v1 to v2.
Why the change? The v1 API used a proprietary query syntax that limited what you could do with your usage data. The v2 API is built on OData V4, an open standard published by OASIS and adopted across the SAP product portfolio. By aligning with OData V4, the Usage Records API becomes a first-class citizen in the SAP ecosystem:
- SAP tooling works out of the box — SAP BTP SDKs, CAP, SAP Fiori elements, and SAP Analytics Cloud all speak OData natively; no custom integration layer needed
- Code generation — OData-aware generators can produce typed client code directly from the
/$metadataendpoint - Broad ecosystem support — any OData V4-compatible client library, IDE extension, or BI tool (Power BI, Excel Power Query, and many open-source libraries) connects without custom adapters
- Consistency across SAP — customers already familiar with OData from SAP S/4HANA, SAP SuccessFactors, or SAP Ariba can apply the same patterns and tooling here immediately
What's New in v2
Richer, More Flexible Data Retrieval
The retrieve operation is the most significant upgrade in v2. The v2 OData API introduces standard query options that give you precise control over what data is returned, and how much of it.
$select — Return Only the Fields You Need
v1 always returns all fields. With $select, you choose exactly which fields come back, reducing payload size and improving performance.
?$select=ID,metricId,technicalResourceId,startedAt,endedAt,quantity,assigned
$expand — Navigate Related Data in a Single Request
v1 returns items embedded by default with no control over depth. v2 gives you full control:
# Records with items and their fieldsGET /UsageRecords?$expand=items($expand=fields)
# Combine $expand with $select at every level
GET /UsageRecords
?$select=ID,metricId,quantity
&$expand=items(
$select=itemNumber;
$expand=fields($select=fieldCode,stringValue)
)
Lambda Operators — Filter by Related Entity Values
This was not possible in v1. OData lambda operator any lets you filter records based on the content of their related collection navigation properties.
GET /UsageRecords
?$filter=customReferences/any(c:c/typeCode eq ‘SYSTEM_TYPE')
Standard $filter with New Functions
The proprietary filter= parameter is replaced by standard OData $filter=. All operators from v1 are supported, plus string functions:
$filter=startswith(technicalResourceId,'device-‘)
$filter=metricId in (‘EMAIL_SENT','EMAIL_RECEIVED')
The total count moves from the x-count response header into the response body:
“@context”: “$metadata#UsageRecords”,
“@count”: 1432,
“value”: [ … ]
}
Batch Operations
v1 $batch used a proprietary JSON format and only supported create/upsert. v2 batch follows the standard OData V4 multipart/mixed format, supporting upsert, discard, delete, and retrieve operations in a single request.
Migration Guide
1. Update the Base URL
Change /v1 to /v2 in the base URL. OAuth scopes are unchanged.
2. Rename Changed Fields
| v1 Field | v2 Field |
userTechnicalId | technicalResourceId |
isAssigned | assigned |
changedAt | modifiedAt |
changedBy | modifiedBy |
subscriptionNumber | subscriptionDocumentNumber |
All other field names are unchanged.
3. Migrate Write Operations
Single Record Create / Upsert
v1
Content-Type: application/json
{
“category”: “MEASURED”,
“metricId”: “API_CALL”,
“userTechnicalId”: “ce6ad4d2-…”,
“startedAt”: “2026-09-01T00:00:00Z”,
“endedAt”: “2026-10-01T00:00:00Z”,
“quantity”: 1200
}
v2
POST /UsageRecords/upsertByLogicalKeyContent-Type: application/json
{
“value”: {
“category”: “MEASURED”,
“metricId”: “API_CALL”,
“technicalResourceId”: “ce6ad4d2-…”,
“startedAt”: “2026-09-01T00:00:00Z”,
“endedAt”: “2026-10-01T00:00:00Z”,
“quantity”: 1200
}
}
Batch Operations
v2 $batch — Batch upsert example
Note: A changeset groups write operations atomically — either all succeed or all are rolled back. It is optional; write requests can also be sent individually in a batch without a changeset.
Content-Type: multipart/mixed; boundary=batch_id-1778814615130-767
–batch_id-1778814615130-767
Content-Type: multipart/mixed; boundary=changeset_1
–changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1-1
POST UsageRecords/upsertByLogicalKey HTTP/1.1
Content-Type: application/json
{
“value”: {
“category”: “MEASURED”,
“metricId”: “API_CALL”,
“technicalResourceId”: “…”,
“startedAt”: “2026-09-01T00:00:00Z”,
“endedAt”: “2026-10-01T00:00:00Z”,
“quantity”: 400
}
}
–changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1-2
POST UsageRecords/upsertByLogicalKey HTTP/1.1
Content-Type: application/json
{
“value”: {
“category”: “MEASURED”,
“metricId”: “E_CHARGE”,
“technicalResourceId”: “…”,
“startedAt”: “2026-09-01T00:00:00Z”,
“endedAt”: “2026-10-01T00:00:00Z”,
“quantity”: 1
}
}
–changeset_1–
–batch_id-1778814615130-767–
Discard / Delete
# Discardv1: POST /usage-records/{id}/discard
v2: POST /UsageRecords({id})/discard
# Delete
v1: DELETE /usage-records/{id}
v2: DELETE /UsageRecords({id})
4. Migrate Read Operations
List Usage Records
v1
GET /usage-records?filter=metricId eq ‘API_CALL'
and startedAt ge 2026-09-01T00:00:00Z
&sort=startedAt desc
&pageSize=50&pageNumber=1
v2
GET /UsageRecords?$filter=metricId eq ‘API_CALL'
and startedAt ge 2026-09-01T00:00:00Z
&$orderby=startedAt desc
&$top=50
Pagination
The server automatically paginates results and returns @nextLink whenever more records exist. Always follow @nextLink to retrieve all pages.
Case 1 — No $top specified (default page size: 16)
GET /UsageRecords# Response — up to 16 items, nextLink present if more exist
{
“value”: [ …up to 16 items… ],
“@nextLink”: “UsageRecords?$skiptoken=…”
}
# Follow nextLink to get the next page
GET /UsageRecords?$skiptoken=…
Case 2 — $top exceeds 100 (silently capped)
If $top is greater than 100, the server caps the result at 100 and returns @nextLink for the remainder — no error is returned.
# Response — only 100 items returned, rest via nextLink
{
“value”: [ …100 items… ],
“@nextLink”: “UsageRecords?$top=500&$skiptoken=…”
}
Filter on Custom References
v1 — Flat dot notation only
?filter=customReferences.typeCode eq ‘SYSTEM_TYPE'v2 — Lambda operator
?$filter=customReferences/any(c:c/typeCode eq ‘SYSTEM_TYPE')
5. Update Response Handling
v2 wraps collection responses in a standard OData envelope. Extract records from the value array; read count from @countinstead of the x-count header.
v1 — Plain array
[{“id”: “992e46d8-…”,
“isAssigned”: true,
“changedAt”: “2026-09-15T12:00:00Z”
}]
v2 — OData envelope
{“@context”: “$metadata#UsageRecords”,
“@count”: 1,
“value”: [{
“ID”: “992e46d8-…”,
“assigned”: true,
“modifiedAt”: “2026-09-15T12:00:00Z”
}]
}
Migration Checklist
- Change
/v1to/v2in the base URL - Rename
userTechnicalId→technicalResourceId,isAssigned→assigned,changedAt→modifiedAt,changedBy→modifiedBy,subscriptionNumber→subscriptionDocumentNumber - Change single-record write:
POST /usage-records→POST /UsageRecords/upsertByLogicalKeywith{ “value”: { … } } - Change batch format: JSON → multipart/mixed
- Change
filter=→$filter=,sort=→$orderby= - Replace
pageNumber/pageSizewith@nextLink(recommended) or$top/$skip - Update count:
x-countheader →@countin response body - Update response parsing: root array →
.valuearray - Update discard/delete URLs:
/{id}/discard→({id})/discard,/{id}→({id}) - (Recommended) Add
$selectto reduce payload on read-heavy integrations - (Recommended) Replace multi-request patterns with
$expand
Resources
- API Guide — Usage Records
- SAP Business Accelerator Hub — Usage Records v2
- OData V4 Query Options Reference
Source link