Consuming the Open Commerce API from SalesForce Commerce Cloud
OCAPI: Why is it there and what can we do with it?
OCAPI is one of the APIs that Salesforce Commerce Cloud offers on its platform.
Another API called Salesforce Commerce API also exists, but we will focus on OCAPI in this article.
One of its common uses are:
- PWA kit (the architecture that will succeed SFRA) uses it along with SCAPI to feed storefront data into its headless frontend components.
- Managing user roles and privileges.
- Gathering data for analytics.
There are three categories of OCAPI endpoints:
- Data API: Contains mostly endpoints to resources that are of interest to merchants (Catalogues, products, inventory etc..)
- Shop API: Contains endpoints that are associated with the shopper experience (Customer password, basket/cart, orders etc..)
- Meta API: Has endpoints to retrieve information about the resources available in OCAPI and their attributes
The goal of this tutorial would be to consume the Open Commerce API and retrieve resources through it on a Spring Boot backend.
To keep things simple for this tutorial we will present these resources through a single controller.
These are the steps that we will follow:
Step 1: Getting the ClientID and client secret from the Account Manager
Step 2: Enabling Endpoints from the Business Manager
Step 3: Using ClientId and ClientSecret to authenticate and retrieve the access token
Step 4: Accessing endpoints from the Open Commerce API
Step 1: Getting the ClientID and client secret from the Account Manager
- Open Account Manager.
- Click API Client. (If you cannot find the API Client section, ask your organization administrator for access)
- Click Add API Client.
- Enter the client’s display name.
- Enter the password twice.
- Locate the organization where you want to add a client, select it, and click Add.
- Click Save.
- Select API Client in the left navigation menu.
- Verify that the new client ID is enabled.
Step 2: Enabling Endpoints from the Business Manager
From the Business Manager > Administration > Site Development > Open Commerce API Settings
We can select the API Type and the Context (scope), then we fill the text area to manage resource access privileges, attribute read/write permissions, or configure client application-specific response headers.
We can specify the Authorized ClientID’s and their corresponding resources access.
{
"_v" : "23.2",
"clients":[
{
"client_id":"YOUR_CLIENT_ID_HERE",
"resources":[
{
"resource_id":"/sites",
"methods":[
"get"
],
"read_attributes":"()",
"write_attributes":"()"
},
{
"resource_id":"/catalogs",
"methods":[
"get"
],
"read_attributes":"()",
"write_attributes":"()"
},
{
"resource_id":"/users/",
"methods":[
"put",
"get",
"patch",
"delete"
],
"read_attributes":"()",
"write_attributes":"()"
}
]
}
]
}
Step 3: Using ClientId and ClientSecret to authenticate and retrieve the access token
In order to retrieve the access token that will allow us to make the API calls, we need to authenticate using the credentials obtained on step 1.
We make the request on the following URL: https://account.demandware.com/dw/oauth2/access_token
@FeignClient(name = "OCAPIAuthenticationClient", url = "https://account.demandware.com")
public interface OCAPIAuthenticationClient {
@RequestMapping(method = RequestMethod.POST, value = "/dw/oauth2/access_token", consumes = "application/x-www-form-urlencoded")
AccessObject authenticate(@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("grant_type") String grantType);
}
The grantType needed for this iauthentication is: client_credentials
. After sending the request, we receive the following JSON:
{
"access_token" : "the access token is normally in here",
"scope" : "mail tenantFilter openId profile roles",
"token_type" : "Bearer",
"expires_in" : "1799"
}
We use AccessObject that allows easy deserialization of the JSON we received and it is defined as follows :
public class AccessObject {
@JsonProperty("access_token")
public String accessToken;
@JsonProperty("scope")
public String scope;
@JsonProperty("token_type")
public String tokenType;
@JsonProperty("expires_in")
public String expiresIn;
}
Now that we got our access token, we can use it in future calls to the Open Commerce API.
Step 4: Accessing endpoints from the Open Commerce API
Using the access token obtained in the authentication, we can make calls to different endpoints of the OCAPI. The URL pattern is as follows:
We should first know what URLs to use in order to access the information from OCAPI :
Base URL:
https://YOUR_INSTANCE.dx.commercecloud.salesforce.com/s/-/dw
To access the DATA, META or SHOP API, we add it to the base URL, so we get the following URL
https://YOUR_INSTANCE.dx.commercecloud.salesforce.com/s/-/dw/data_type
Then we add the OCAPI version
https://YOUR_INSTANCE.dx.commercecloud.salesforce.com/s/-/dw/data_type/v21_3
Then finally we add the End Points that can be found on the following link : OCAPI Documentation
In this example, we will get the sites in our organization through an API call to the DATA API. We use a Feign Client interface with the base URL : "https://YOUR_INSTANCE.commercecloud.salesforce.com/s/-/dw"
, then define a GET method to access the GET Sites or GET Catalogs endpoint.
@FeignClient(name = "OCAPICallsClient", url = "https://YOUR_INSTANCE.commercecloud.salesforce.com/s/-/dw")
public interface OCAPICallsClient{
@RequestMapping(method = RequestMethod.GET, value = "/data/v21_3/sites")
String getSites(@RequestHeader("Authorization") String token);
@RequestMapping(method = RequestMethod.GET, value = "/data/v21_3/catalogs")
String getCatalogs(@RequestHeader("Authorization") String token);
}
Notice that we didn’t put the authentication and api call methods in the same Feign Client Interface because they don’t have the same Base_URL.
We can access other Endpoints. To get Orders, for example, we need to access the SHOP API, and we can do that as follows :
@RequestMapping(method = RequestMethod.GET, value = "/shop/v21_3/orders")
String getOrders(@RequestHeader("Authorization") String token);
Notice that in both methods, we put the token received through the authorization into the request header, and it should be preceded by the word Bearer as follows :
RequestHeader (Authorization : Bearer <access token>)
Creating the OCAPIService:
public class OCAPIService {
private final OCAPIAuthenticationClient authenticationFeignClient;
private final OCAPICallsClient callsFeignClient;
private String accessToken;
public AccessObject authenticate(String clientId, String clientSecret) {
return authenticationFeignClient.authenticate(clientId, clientSecret, "client_credentials");
}
public String getOrders() {
return callsFeignClient.getOrders(accessToken);
}
public String getCatalogs() {
return callsFeignClient.getCatalogs(accessToken);
}
public String getSites() {
return callsFeignClient.getSites(accessToken);
}
}
Now we need a controller to access the endpoints from our localhost:
@RequestMapping("/ocapi")
public class OCAPIController {
private final OCAPIService ocapiService;
@GetMapping("/orders")
public String getOrders() {
Authenticate();
return ocapiService.getOrders();
}
@GetMapping("/sites")
public String getSites() {
Authenticate();
return ocapiService.getSites();
}
@GetMapping("/catalogs")
public String getCatalogs() {
Authenticate();
return ocapiService.getCatalogs();
}
public void Authenticate() {
AccessObject accessObject = ocapiService.authenticate("YOUR_CLIENT_ID", "YOUR_CLIENT_PASSWORD");
ocapiService.setAccessToken("Bearer " + accessObject.getAccessToken());
}
}
If we access: http://localhost:8080/ocapi/sites we can obtain the following result:
{
"_v":"21.3",
"_type":"sites",
"count":7,
"data":[
{"_type":"site","_resource_state":"300a33a6725b54a50fe5d5844bfc2d1997e01441cca4e6ec901ac10aee92068a","id":"RefArch","link":"https://znbf-001.dx.commercecloud.salesforce.com/s/-/dw/data/v21_3/sites/RefArch"},
{"_type":"site","_resource_state":"22c2f60b22d8d18590921b69385b607321993c9577a884a70e0581ee04eaf357","id":"RefArchGlobal","link":"https://znbf-001.dx.commercecloud.salesforce.com/s/-/dw/data/v21_3/sites/RefArchGlobal"},
{"_type":"site","_resource_state":"c3cd3ef0a5dab5e6f5900ce07917b2c1899a23784c117dcd649441959af4ab7e","id":"SiteGenesis","link":"https://znbf-001.dx.commercecloud.salesforce.com/s/-/dw/data/v21_3/sites/SiteGenesis"},
{"_type":"site","_resource_state":"d618ce1d22447cde3e0693aced82cb682de4f7c6d7ffdae2887f7b4eb6ba58f9","id":"SiteGenesisGlobal","link":"https://znbf-001.dx.commercecloud.salesforce.com/s/-/dw/data/v21_3/sites/SiteGenesisGlobal"}],
"start":0,
"total":7
}
Bonus: Hooks
Hooks empower developers with a powerful toolset to inject your own customized code into OCAPI resources, allowing to enhance the server-side behaviour of your online store.
These are the types of hooks that are available:
- before<HTTP_Method>: Execute custom code before the server performs its processing.
- after<HTTP_Method>: Execute custom code after the server’s processing.
- modify<HTTP_Method>Response: Apply final changes to the response document.
It can possibly be used to extend response data, call other APIs or filter a response.
This sounds difficult to grasp without a frame of reference to fall back on. Thankfully Salesforce provided us with a collection of hooks on their repo SFCC Hooks Collection and that is what we are going to look at so that we can dissect the concept of hooks.
Step 1: Enable OCAPI hooks
To enable API hooks follow these steps:
- Navigate to Administration > Global Preferences > Feature Switches.
- Check the “Enable Salesforce Commerce Cloud API hook execution” feature switch.
Step 2: Set the OCAPI hooks and their scripts
All the hooks you develop in your cartridge should be registered in a hooks.json file which would contain the paths to each of your hook scripts and their corresponding resources.
That hooks.json file should also be declared in the package.json file by adding this attribute:
{
"hooks": "./cartridge/scripts/hooks.json"
}
and this what the hooks.json file would look like:
{
"hooks": [
{
"name": "dw.ocapi.shop.basket.billing_address.beforePUT",
"script": "./basket_hook_scripts.js"
},
{
"name": "dw.ocapi.shop.basket.billing_address.afterPUT",
"script": "./basket_hook_scripts.js"
}
]
}
The path to the hook is declared in the hooks.json
file along with the resource that it will hook into.
So technically you could put your hooks anywhere you like but by convention they’re mostly under this path ./cartridge/scripts/hooks/*.js
.
We’ll take a look at an example hook from the collection we mentioned earlier:
the resource that this hook is injected into it is: dw.ocapi.shop.product_search.modifyGETResponse
So it would be called whenever a product is searched for.
exports.modifyGETResponse = function (searchResponse) {
if (searchResponse && searchResponse.count > 0) {
var hits = searchResponse.hits.toArray();
hits.forEach(function (hit) {
if (hit.represented_product) {
var productApi = require('*/cartridge/scripts/apis/productExtend');
hit.c_extend = productApi.createExtendedProduct(hit.represented_product.id);
}
});
}
};
This simple hook intercepts the the response that you get whenever you search for a product.
Loops through all the products in the search result and extends them with more attributes using this custom method productApi.createExtendedProduct
You can look at it in more detail here: Create Extended Product Hook Script