APEX: OCI Cost Usage
Grouping REST Response for use in Summary Reports and Dashboards

I work with software but it does not define me and my constant is change and I live a life of evolution. Learning, adapting, forgetting, re-learning, repeating I am not a Developer, I simply use software tools to solve interesting challenges and implement different use cases.
Cloud positioning and pressures are here. Deals have been signed, enterprise services migrations have been budgeted, and the journeys are well on their way.
The initial fears to "Trust" service and cloud providers may have been allayed to some degree but do providers really give the cost benefit they touted, do budgets match the consumption?
About this Post
This month, curiously, a few customers asked myself, on separate projects, to provide some answers to these questions. Although the console does provide reports and dashboards, it can be confusing and unclear to interpret.
The task at hand was to visualization this information in APEX, by consuming the OCI Cost Usage APIs. OCI does offer several SDK options, including the PL/SQL SDK Usage API.
I'll tackle the first and simplest of the two, the REST API.
OCI Billing and Cost Management Overview
The documentation states that:
It even has a OCI FinOps Hub and a Cloud Advisor, so why build it in APEX? 🤔
Policy management, segregated views, access to the cloud console in itself could contribute to the decision to build a dedicated app but I don't have to debate such things. I simply need to support the customers who choice to do so.
Let's get into it ...
OCI Cost Usage API
The Usage API follows the typical OCI REST API format, it has a base endpoint per region, for me, in the Frankfurt region, it is => https://usageapi.eu-frankfurt-1.oci.oraclecloud.com
It provides APIs to few the calculated Carbon Emission for your services, how much of the Energy was Clean and Green, and of course the Usage Summaries, which is what I am interested in.
This returns the summarized totals across the tenant and can be filtered to match you requirement.
You will have to navigate the OCI documentation to assemble all of the types for the request, and if you are struggling, check out my previous blog on how to understand and RTF=>OCI-M.
OCI Cost Usage API APEX REST Templates
Required Attributes Only Template
From the request body details there are only three required attributes
Only four attributes are required: granularity, tenantId, timeUsageEnded, & timeUsageStarted
{
"granularity": "DAILY",
"tenantId": "#TENANTID#",
"timeUsageEnded": "#USAGEENDED#",
"timeUsageStarted": "#USAGESTARTED#"
}
This is not recommended as the response will not be very useful.
{
"groupBy" : null,
"items" :
[
{
"tenantId" : null,
"tenantName" : null,
"compartmentId" : null,
"compartmentPath" : null,
"compartmentName" : null,
"service" : null,
"resourceName" : null,
"resourceId" : null,
"region" : null,
"ad" : null,
"weight" : null,
"shape" : null,
"skuPartNumber" : null,
"skuName" : null,
"unit" : null,
"discount" : null,
"listRate" : null,
"platform" : null,
"timeUsageStarted" : "2026-03-20T00:00:00.000Z",
"timeUsageEnded" : "2026-03-21T00:00:00.000Z",
"computedAmount" : 2611.391053413421,
"computedQuantity" : 809275.68046217912,
"attributedCost" : "2611.391053413421",
"attributedUsage" : "809275.680462179313",
"overagesFlag" : null,
"unitPrice" : null,
"currency" : "USD",
"subscriptionId" : null,
"overage" : null,
"isForecast" : false,
"tags" :
[
{
"namespace" : null,
"key" : null,
"value" : null
}
]
},
{
"tenantId" : null,
"tenantName" : null,
"compartmentId" : null,
"compartmentPath" : null,
"compartmentName" : null,
"service" : null,
"resourceName" : null,
"resourceId" : null,
"region" : null,
.....
}
Recommended Base Template
The previous response was not very useful as most of the column values were null. The APIs is used to provide summary aggregates, but without the groupBy clause, it basically does nothing.
I did not expect this and would not find this API useful without the aggregation, and hence groupBy should be required, but it is not.
If a complete view is what is required, then several passes will be be needed as only 4 items are supported in the groupBy attribute.
For example, if I wanted to know the cost per tenant, across regions, per service, across LOB compartments and budget or cost Tag, I couldn't.
I would need to get higher level values first, then make another pass for the lower attributes while including some common attribute across the queries.
{
"compartmentDepth": 7,
"tenantId": "#TENANTID#",
"timeUsageEnded": "#USAGEENDED#",
"timeUsageStarted": "#USAGESTARTED#",
"granularity": "DAILY",
"groupBy": ["tagValue","service","compartmentName","compartmentId"],
"filter": {
"operator": "OR",
"tags": [{
"key": "CreatedBy",
"namespace": "Default_Tags",
"value": "#DOMAINUSERNAME#"
}]
}
}
In the provided example, I want to have the Tag values, which is nested in the response.
Strangely, not all columns return a value and similarly the groupBy does not support all of the columns that are available.
You can see that the resourceName and even for resources like Compute the ad, are always null
{
"groupBy" :
[
"tagValue",
"service",
"compartmentName",
"compartmentId"
],
"items" :
[
{
"tenantId" : null,
"tenantName" : null,
"compartmentId" : "ocid1.compartment.oc1..OCID",
"compartmentPath" : null,
"compartmentName" : "SydneyNurse",
"service" : "Database",
"resourceName" : null,
"resourceId" : null,
"region" : null,
"ad" : null,
"weight" : null,
"shape" : null,
"skuPartNumber" : null,
"skuName" : null,
"unit" : null,
"discount" : null,
"listRate" : null,
"platform" : null,
"timeUsageStarted" : "2026-03-04T00:00:00.000Z",
"timeUsageEnded" : "2026-03-05T00:00:00.000Z",
"computedAmount" : 8.69055801355,
"computedQuantity" : 1328.978745298711,
"attributedCost" : "8.69055801355",
"attributedUsage" : "1328.978745298711",
"overagesFlag" : null,
"unitPrice" : null,
"currency" : "USD",
"subscriptionId" : null,
"overage" : null,
"isForecast" : false,
"tags" :
[
{
"namespace" : null,
"key" : null,
"value" : "bigDomain/sydney.nurse@oracle.com"
}
]
},
{
"tenantId" : null,
"tenantName" : null,
"compartmentId" : "ocid1.compartment.oc1..OCID",
"compartmentPath" : null,
"compartmentName" : "SydneyNurse",
"service" : "Database",
"resourceName" : null,
"resourceId" : null,
"region" : null,
....
}
I also added a filter example but as I noted earlier the TypeScript example is very good.
The REST Data Source
Manually create the REST Data Source, set it up with a POST method and provide the Request Template.
Parameterize it as required and synchronize it with the Request Body.
Provide some default values that will allow you to test the POST method.
**This could be due to my level of granularity**
Building Visualizations
For the above screenshot, I created a blank page with two Page Items for the Start and End Dates.
I used YYYY-MM-DD HH24:MI:SS for my format but this can be anything or null to take the app defaults
I set my defaults for the previous Month Start and End day.
BEGIN
:P102_START_DATE :=
TO_CHAR(TRUNC(ADD_MONTHS(TRUNC(CAST(CURRENT_TIMESTAMP
AS DATE), 'MM'), -1), 'MM'),'YYYY-MM-DD HH24:MI:SS');
:P102_END_DATE :=
TO_CHAR(TRUNC(CAST(CURRENT_TIMESTAMP
AS DATE), 'MM'), 'YYYY-MM-DD HH24:MI:SS');
END;
I used the Default property but shown here in a single PL/SQL block.
REST Source Parameters
The Dates for the REST Source are the key attributes that need formatting. This was the trickiest area to get correct and with the default values, the Page Items, need to be formatted as required: 'YYYY-MM-DD"T"HH24:MI:SS".000Z"'
Set the Parameters to use a PL/SQL Expression
-- USAGESTARTED
to_char(to_timestamp(:P102_START_DATE, 'YYYY-MM-DD HH24:MI:SS'),'YYYY-MM-DD"T"HH24:MI:SS".000Z"')
-- USAGEENDED
to_char(to_timestamp(:P102_END_DATE, 'YYYY-MM-DD HH24:MI:SS'),'YYYY-MM-DD"T"HH24:MI:SS".000Z"')
Charts
For the Totals per Day, I used an Area Chart sourcing the REST Source. I submitted the start and end date page items and set the Data Profile to the Tags nested rows.
I would like the values to be stacked, gaps filled and I set the Time Axis Type to Automatic.
I used a Donut chart for the Totals per Service, nothing special in the settings
Additional Groupings
While inspecting the response, you will notice that the summaries are aggregated at the granularity level, so for the chart, some additional rollup was required.
Using the Local Post Processing option, I adapt the response to my requirements
select
SERVICE,
SUM(ATTRIBUTEDCOST) COST,
TIMEUSAGEENDED,
TIMEUSAGESTARTED
from #APEX$SOURCE_DATA#
GROUP BY SERVICE, TIMEUSAGESTARTED, TIMEUSAGEENDED
ORDER BY SERVICE
I used the same for both regions but adapt it to suite the report and visualization.
Series
Using the post processing columns, I set the region series as follows:
Total Costs per Day
Column Mapping:
Series Name: SERVICE
Label: TIMEUSAGESTARTED
Value: COST
Total Costs per Series
Column Mapping:
Series Name: SERVICE
Label: SERVICE
Value: COST
Conclusion
Consuming OCI REST APIs, stays the same, the process to navigate and understand the documentation has not changed.
The Cost Usage API is a little different as data will be aggregated at source using the groupBy attribute. It only supports a single HTTP POST method, so auto-discovery is not the best option when creating the REST Data Source, use Manual.
Large customer deployments that span multiple, tenants and regions, may need to make several passes to get a complete picture at the right level for reporting requirements.
Synchronizing this data for monthly reports would be advantageous and provide users with faster screen responses by building on top of tables and views for the additional aggregation.
I've not delved into the Forecasting but this is a nice option for budgeting discussion to estimate overages or during the next round of negotiations.



