Custom APIs

B2C Commerce provides powerful tools to customize shopper experiences. Today SCAPI can be customized via Hooks, where developers use the B2C Commerce Script API to add filters or modify responses before they get sent back to the calling application. A composable storefront calls custom APIs via hooks. Storefront Next calls them either as a generated typed client built on the SCAPI client primitives, or directly with native fetch.

To further leverage existing development tools, B2C Commerce provides a framework enabling developers to write custom script code, such as controllers, and expose this functionality as a custom REST API under the SCAPI framework.

The SCAPI Custom APIs video walks you through how to build a cartridge with a Custom API. If you do not have access to the code samples provided in the video, see Salesforce Commerce Cloud GitHub Repositories and Access.

SCAPI Custom APIs

Overview 

A Custom API URL has the following structural format:

https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}/organizations/{organizationId}/{endpointPath}

The term custom must be used as API family. The parameters apiName, apiVersion, and endpointPath can be defined as described below.

In order to create a Custom API, three components are required:

  • The API contract as an OAS 3.0 schema file.
  • The API implementation as a script using the B2C Commerce Script API.
  • The API mapping defined in an api.json file.

In this guide, we will look at an example called Loyalty Info API that can be used to retrieve the loyalty status for a given customer. This graphic shows the required components and their relationship:

Custom API integration with B2C Commerce platform

Only existing customers can access some of the links on this page. Visit Salesforce Commerce Cloud GitHub Repositories and Access for information about how to get access to the repositories.

Tip

The components will be further explained in the next sections. Note the following details from the diagram:

  • A dashed line shows where this URL segment is defined.
  • The blue parameter is the endpoint identifier. It is used to bind the endpoint to the implementation and appears in all three components.

Cartridge 

Custom APIs are defined within custom code cartridges. In the cartridge directory, a new folder rest-apis must be created which is the root folder for all APIs in that cartridge.

In the rest-apis folder, subdirectories represent the actual APIs. The names of these directories represent the API names as they are addressed by the URL. Each directory contains the files for the three components representing this API.

Example: The structure for the example API called loyalty-info is expected to look like this:

Cartridge structure
1/my-cartridge
2    /cartridge
3        package.json
4        /rest-apis
5            /loyalty-info
6                api.json
7                schema.yaml
8                schema_v2.yaml
9                script.js
10                script_v2.js
11        /pipelines
12        /scripts
13        /controllers

API directory names can only contain alphanumeric lowercase characters and hyphens.

Important

Due to the registration process, it is recommended to start implementing Custom API endpoints in a new code version.

Tip

Contract 

The API contract must be provided by an OAS 3.0 schema file in YAML format. For more information about OAS 3.0, refer to the OAS 3.0 specification.

In this schema file, the API version and the endpoints are defined. The API version is located in the info.version field and is transformed into a URL version by using the major version segment and prepending it with v (for example, 1.0.1v1).

The endpoints are defined as part of the paths object and can implement multiple operations (that is, HTTP methods). Each operation is required to have a property called operationId, which must be unique within the schema file. It is used for mapping the endpoint to the implementation. The path of the endpoint becomes the endpointPath in the URL structure.

To secure the endpoint, define a valid security scheme as a shared component and apply it at the endpoint or global level. See Authentication for more details.

Example: The schema for a /customers?c_customer_id={id} endpoint in version v1 is expected to look like this:

schema.yaml:
1openapi: 3.0.0
2info:
3  version: 1.0.1
4  title: Loyalty Information API
5components:
6  securitySchemes:
7    ShopperToken:
8      type: oauth2
9      flows:
10        authorizationCode:
11          authorizationUrl: https://my-shortcode.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/my-org-id/oauth2/authorize
12          tokenUrl: https://my-shortcode.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/my-org-id/oauth2/token
13          scopes:
14            c_loyaltyInfo_r: Read loyalty information of a customer
15        clientCredentials:
16          tokenUrl: https://my-shortcode.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/my-org-id/oauth2/token
17          scopes:
18            c_loyaltyInfo_r: Read loyalty information of a customer
19paths:
20  /customers:
21    get:
22      summary: Get loyalty information for customer
23      operationId: getLoyaltyInfo
24      parameters:
25        - in: query
26          name: c_customer_id
27          required: true
28          schema:
29            type: string
30        - in: query
31          name: siteId
32          required: true
33          schema:
34            type: string
35            minLength: 1
36        - in: query
37          name: locale
38          required: false
39          schema:
40            type: string
41            minLength: 1
42      responses:
43        "200":
44          description: Shows loyalty information for the customer
45          content:
46            application/json:
47              schema:
48                type: object
49                properties:
50                  tier:
51                    type: string
52                  points:
53                    type: integer
54        "404":
55          description: Shows that the customer is not found
56          content:
57            application/json:
58              examples:
59                not_found:
60                  value:
61                    {
62                      "type": "https://api.commercecloud.salesforce.com/documentation/error/v1/custom-errors/customer-not-found",
63                    }
64security:
65  - ShopperToken: ["c_loyaltyInfo_r"]

Implementation 

The API implementation is provided by a script file using the B2C Commerce Script API. The function name must match the value of the endpoint’s operationId in the contract.

Example: A valid implementation for /customers?c_customer_id={id} based on the definition in the contract could look like this:

script.js:
1var RESTResponseMgr = require("dw/system/RESTResponseMgr");
2
3exports.getLoyaltyInfo = function () {
4
5  var customerId = request
6    .getHttpParameterMap()
7    .get("c_customer_id")
8    .getStringValue();
9
10  /*
11    ... some lookup of loyalty data based on customer id ...
12  */
13
14  if (customerId === "customer1") {
15
16    var info = {
17      tier: "silver",
18      points: 14275
19    };
20
21    RESTResponseMgr
22      .createSuccess(info)
23      .render();
24
25  } else {
26
27    RESTResponseMgr
28      .createError(404, "customer-not-found", "Customer Unknown", "You provided an unknown customer ID.")
29      .render();
30
31  }
32
33};
34
35exports.getLoyaltyInfo.public = true;

Always return responses in JSON format. Errors should additionally adhere to the SCAPI error format as described in RFC 9457, with at least the type field being present. Response bodies are subject to validation in the future and may be declined if they don’t meet these requirements.

Important

Make sure that the implementation contains high quality, well-tested code with sufficient error handling. In order to protect the platform, Custom APIs use a Circuit Breaker mechanism that will block API requests when the implementation throws too many errors. See Circuit Breaker for more information.

Important

To learn how to create JSON responses in your scripts with ease, have a look at the documentation of the Script API class RESTResponseMgr.

Tip

Mapping 

The api.json file provides the API mapping. It contains a list of endpoints, and for each entry the endpoint name, its schema file, and the name of the implementing script is defined.

Example: A valid mapping for /customers?c_customer_id={id} based on the definitions in the contract and implementation is expected to look like this:

api.json:
1{
2  "endpoints": [
3    {"endpoint": "getLoyaltyInfo", "schema": "schema.yaml", "implementation": "script"},
4    {"endpoint": "getLoyaltyInfo", "schema": "schema_v2.yaml", "implementation": "script_v2"}
5  ]
6}

Relative paths are not supported. The schema and implementation must be located at the same level as the corresponding api.json.

Important

Provide the implementation name without the file extension.

Important

Registration of Endpoints 

When the Custom API endpoints have been correctly defined as previously described, they must be registered in order to be accessible.

The registration is triggered by activating the code version containing the new API definitions. Whenever changes to an API are required, it is recommended to work in a new code version and to switch the version when ready.

Versioning 

Custom API versions are represented in two different formats, the API version and the URL version.

The API version is defined in the contract as part of the info.version field and applies to all endpoints defined in this file. The value must be numeric but can have multiple segments separated by a dot.

In the URL, the version is transformed by using the major version segment and prepending it with v . For example, this list shows valid API versions and their corresponding URL versions:

  • 1 → v1
  • 1.0 → v1
  • 2.0 → v2
  • 2.1.1 → v2

It is recommended to introduce breaking changes in a new major version. See the SCAPI Change Policy for details.

System Query Parameters 

The following system query parameters exist and can be used when defined in the contract:

  • siteId - The site of the current request
  • locale - The locale of the current request

Make sure you understand the meaning of these parameters and when to use them. For example, the system uses the siteId to identify the site context, so using this parameter only makes sense for a Shopper API request. For an Admin API request that requires a site, for example: an API that returns objects filtered by site, use a custom query parameter like c_siteId. Note that the site is a query parameter instead of a path parameter, which is different from other ways of customization, such as using controllers and hooks. The parameter locale is used to set the locale of the request, and behaves the same way as the Script API method dw.system.Request#setLocale.

If a siteId is not provided, the request is treated as a SCAPI Admin API request and the Business Manager site is used. If a locale is not provided in a request, the site’s default locale is used.

The parameter siteId is also used to register a Custom API. This means that if you don’t add the cartridge containing a Custom API to a site cartridge path, any request to it with that siteId results in a 404 response. For Admin APIs, the cartridge must be added to the cartridge path of the Business Manager site.

Note

Don’t use a shopper token for an Admin API request. In these scenarios, use an AmOAuth2 token.

Example 1: Define the system query parameter using the name for siteId and type string. If the parameter isn’t a valid string, the request is declined.

1- in: query
2  name: siteId
3  required: true
4  schema:
5    type: string
6    minLength: 1

Example 2: Define the system query parameter with the name locale and the type string. If the parameter isn’t a valid string, the request is declined.

1- in: query
2  name: locale
3  required: false
4  schema:
5    type: string
6    minLength: 1

Example 3: Make a Custom API request with system query parameters:

1GET https://my-shortcode.api.commercecloud.salesforce.com/custom/loyalty-info/v1/organizations/my-org-id/customers?c_customer_id=customer1&siteId=MySite&locale=en-US

Define system query parameters with the correct schema field as shown previously. Setting required to false means that the value is not required. When required is true, the request is refused if the parameter is missing or empty.

Important

Shopper and Admin API Endpoints 

Like the non-custom B2C Commerce APIs (SCAPI), each endpoint is either a Shopper or an Admin API endpoint. For general information on how to differentiate those API groups, see SCAPI Groups.

For Custom APIs, there are three main differences between Shopper and Admin endpoints:

  1. Shopper endpoints have stricter limits for request runtime, request body size, and API quotas. See Limitations for specific values.
  2. Shopper endpoints require the siteId query parameter, while Admin endpoints must omit this parameter. Requests without a siteId are treated as Admin API requests.
  3. Shopper endpoints require the ShopperToken security scheme, while Admin endpoints require the AmOAuth2 security scheme, and each endpoint can have only one security scheme. See Authentication for details on how to use these.

Supported HTTP Methods 

Supported methods for Custom APIs include:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS

The following example extends the Loyalty Information API with a POST method with which an admin can update the loyalty information:

schema.yaml:
1openapi: 3.0.0
2info:
3  version: 1.0.1
4  title: Loyalty Information API
5components:
6  securitySchemes:
7    AmOAuth2:
8      type: oauth2
9      flows:
10        authorizationCode:
11          authorizationUrl: https://account.demandware.com/dwsso/oauth2/authorize
12          tokenUrl: https://account.demandware.com/dwsso/oauth2/access_token
13          scopes:
14            c_loyaltyInfo_rw: Read or Update loyalty information of a customer
15        clientCredentials:
16          tokenUrl: https://account.demandware.com/dwsso/oauth2/access_token
17          scopes:
18            c_loyaltyInfo_rw: Read or Update loyalty information of a customer
19paths:
20  /customers/{customerId}:
21    post:
22      summary: Update loyalty information for customer
23      operationId: updateLoyaltyInfo
24      parameters:
25        - in: path
26          name: customerId
27          required: true
28          schema:
29            type: string
30      requestBody:
31        content:
32          'application/json':
33            schema:
34              type: object
35              required:
36                - points
37              properties:
38                points:
39                  type: number
40                  minimum: 1
41                  maximum: 99999
42      responses:
43        "200":
44          description: Shows loyalty information for the customer
45          content:
46            application/json:
47              schema:
48                type: object
49                properties:
50                  tier:
51                    type: string
52                  points:
53                    type: integer
54        "404":
55          description: Shows that the customer is not found
56          content:
57            application/json:
58              examples:
59                not_found:
60                  value:
61                    {
62                      "type": "https://api.commercecloud.salesforce.com/documentation/error/v1/custom-errors/customer-not-found",
63                    }
64security:
65  - AmOAuth2: ["c_loyaltyInfo_rw"]

Here is a sample implementation for this endpoint:

script.js:
1var RESTResponseMgr = require("dw/system/RESTResponseMgr");
2
3exports.updateLoyaltyInfo = function () {
4
5  var customerId = request
6    .getSCAPIPathParameters()
7    .get('customerId');
8
9  var requestBody = request
10    .httpParameterMap
11    .requestBodyAsString;
12  var requestJSON = JSON.parse( requestBody );
13
14  /*
15    ... Update loyalty points based on customer id and request body ...
16  */
17
18
19  if (customerId === "customer1") {
20
21    var updatedInfo = {
22      tier: "silver",
23      points: requestJSON.points
24    };
25
26    RESTResponseMgr
27      .createSuccess(updatedInfo)
28      .render();
29
30  } else {
31
32    RESTResponseMgr
33      .createError(404, "customer-not-found", "Customer Unknown", "You provided an unknown customer ID.")
34      .render();
35
36  }
37
38};
39
40exports.updateLoyaltyInfo.public = true;

To learn more about new convenience methods for SCAPI path parsing, refer to the Request class in Script API.

Tip

The attribute additionalProperties is NOT allowed when defining the request body schema. To take advantage of request body validation and build robust APIs, define the request body schema in detail. If the request body schema of an endpoint contains additionalProperties, the endpoint is not registered.

Note

Validation 

All request parameters must be defined in the contract as requests with unknown parameters are declined. Custom query parameters must be prefixed with c_. System query parameters must be added to the contract if used.

Example: Defining a mandatory query parameter with the name c_status and the type integer. If the parameter is not present or is not a valid integer, the request is declined.

1/error:
2  get:
3    summary: Throw an error
4    operationId: throwError
5    parameters:
6      - in: query
7        name: c_status
8        required: true
9        schema:
10          type: integer

Sample Custom API Collection 

To get access to this repository, complete the trail Tools & Resources for Salesforce B2C Commerce Developers.

Tip

Our community is maintaining a Custom API collection within the Sample Hooks Collection, providing the functionality earlier offered with hooks as new Custom APIs. Here are a few APIs available with this collection:

  • URL Resolution
  • URL List API
  • Guest Order Look Up
  • Customer Groups API
  • Mini Product
  • Send Mail API
  • Geo IP API

For more information, check out the Custom APIs Collection on the Salesforce Commerce Cloud GitHub repository.

Limitations 

The following limitations are in place:

  • General:
    • Transactions can’t be committed in the case of GET requests.
    • The schema attribute additionalProperties is not allowed.
    • Shopper and Admin APIs must adhere to the applicable API quotas. For quota details, see API Quotas documentation.
  • Shopper APIs:
    • Maximum request runtime of 10 seconds.
    • Maximum request body size of 5 MiB (5,242,880 bytes).
    • STOREFRONT activity type. Storefront quota limits are applied.
  • SCAPI Admin APIs:
    • Maximum request runtime of 60 seconds.
    • Maximum request body size of 20 MB (20,000,000 bytes).
    • BUSINESS_MANAGER activity type. Default quota limits are applied.