Skip to main content

BEYABLE Product Ranking API - Salesforce Commerce Cloud Integration Guide

Overview

This guide provides step-by-step instructions for integrating the BEYABLE Product Ranking API with Salesforce Commerce Cloud (SFCC). The integration enables AI-powered product ranking to optimize your product listings across category pages, search results, and recommendation slots.

Table of Contents

  1. Prerequisites
  2. Architecture Overview
  3. Cartridge Structure
  4. Installation
  5. Configuration
  6. Metadata Validation
  7. Implementation
  8. Job Configuration
  9. Frontend Integration
  10. Testing
  11. Deployment
  12. Cartridge Maintenance & Updates
  13. Monitoring & Troubleshooting
  14. Performance Optimization

Prerequisites

System Requirements

  • Salesforce Commerce Cloud (SFCC/B2C Commerce) - Compatibility Mode 19.10 or higher
  • Business Manager access with Administrator privileges
  • Code deployment access (via VS Code or command line)
  • Node.js 14+ (for local development and testing)
  • SFRA (Storefront Reference Architecture) or SiteGenesis

Required Information from BEYABLE

Obtain the following from your BEYABLE account manager:

  • Account ID: Your 32-character UUID (without hyphens)
  • Subscription Key: Your API authentication key
  • Ranking Rule ID(s): UUID for each ranking rule
  • Segmented Ranking ID(s): UUID for segmented rankings (if applicable)
  • Site/Locale Mapping: Tenant configuration for your SFCC sites

SFCC Knowledge Required

  • Understanding of SFCC cartridges and pipelines/controllers
  • Experience with Business Manager configuration
  • Familiarity with ISML templates
  • Knowledge of SFCC jobs and site preferences

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│ BEYABLE Platform │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Ranking Rules & │ │ Product Ranking API │ │
│ │ Segment Config │───────>│ api.eu1.beyable.com │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└───────────────────────────────────────┬─────────────────────────┘
│ CSV Response
│ (ProductId, Position)

┌─────────────────────────────────────────────────────────────────┐
│ Salesforce Commerce Cloud (SFCC) │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Job: Sync │ │ Custom Object: │ │
│ │ Rankings │───────>│ BEYABLERanking │ │
│ │ (Every 2hrs) │ │ (Store rankings) │ │
│ └──────────────────┘ └──────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Shopper visits │ │ │
│ │ Category/Search │ ▼ │
│ └────────┬─────────┘ ┌──────────────────────────────┐ │
│ │ │ Controller/Script: │ │
│ │ │ - Read segment cookie │ │
│ └─────────────────>│ - Apply rankings │ │
│ │ - Sort products │ │
│ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Optimized Product Listing │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Integration Flow:

  1. BEYABLE Cookie: BEYABLE Platform sets visitor segment cookie
  2. Scheduled Job: SFCC job fetches rankings from BEYABLE API (every 1-4 hours)
  3. Data Storage: Rankings stored in SFCC Custom Objects
  4. Runtime Application: Controllers read segment, apply rankings to product searches
  5. Frontend Display: Optimized product listings displayed to shoppers

Cartridge Structure

int_beyable_ranking/
├── cartridge/
│ ├── client/
│ │ └── default/
│ │ └── js/
│ │ └── beyable/
│ │ └── tracking.js
│ ├── controllers/
│ │ ├── BEYABLERanking.js
│ │ └── Product.js (decorator)
│ ├── experience/
│ │ └── components/
│ │ └── commerce_assets/
│ │ └── productTile.js
│ ├── models/
│ │ ├── beyable/
│ │ │ ├── rankingAPI.js
│ │ │ ├── rankingService.js
│ │ │ └── segmentHelper.js
│ │ └── product/
│ │ └── productSearch.js (decorator)
│ ├── scripts/
│ │ ├── jobs/
│ │ │ └── SyncRankings.js
│ │ ├── helpers/
│ │ │ ├── beyableHelper.js
│ │ │ └── rankingHelper.js
│ │ └── services/
│ │ └── BEYABLEService.js
│ ├── templates/
│ │ └── default/
│ │ ├── search/
│ │ │ └── productGrid.isml (decorator)
│ │ └── components/
│ │ └── beyable/
│ │ └── tracking.isml
│ ├── static/
│ │ └── default/
│ │ └── css/
│ │ └── beyable.css
│ └── int_beyable_ranking.properties
├── metadata/
│ ├── custom-objecttype-definitions.xml
│ ├── jobs.xml
│ ├── services.xml
│ └── site-preferences.xml
├── sites/
│ └── site_template/
│ ├── preferences.xml
│ └── services.xml
├── caches.json
├── package.json
├── VERSION.txt
└── CHANGELOG.md

Installation

Step 1: Create Cartridge

Create the cartridge directory:

cd your-sfcc-project/cartridges
mkdir -p int_beyable_ranking/cartridge
cd int_beyable_ranking

Step 2: Initialize Package

Create package.json:

{
"name": "int_beyable_ranking",
"version": "1.1.0",
"description": "BEYABLE Product Ranking API Integration for SFCC",
"main": "cartridge/scripts/init.js",
"caches": "./caches.json",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint cartridge/",
"upload": "sgmf-scripts --upload"
},
"keywords": [
"SFCC",
"BEYABLE",
"Product Ranking",
"Commerce Cloud"
],
"author": "Your Company",
"license": "PROPRIETARY",
"dependencies": {
"dw-api-types": "^23.2.0"
},
"devDependencies": {
"eslint": "^8.0.0",
"sgmf-scripts": "^2.0.0"
}
}

Note (v1.1): The "caches": "./caches.json" entry is required for the custom cache used in Performance Optimization. See Step 4 below.

Step 3: Cartridge Properties

Create cartridge/int_beyable_ranking.properties:

## Cartridge Properties for BEYABLE Product Ranking Integration
demandware.cartridges.int_beyable_ranking.id=int_beyable_ranking
demandware.cartridges.int_beyable_ranking.multipleLanguageStorefront=true

Step 4: Cache Registration

Create caches.json at the cartridge root. This registers the custom cache used by getCachedRankings() — without this file, CacheMgr.getCache('BEYABLERankings') will throw at runtime:

{
"caches": [
{
"id": "BEYABLERankings",
"expireAfterSeconds": 7200
}
]
}

Configuration

Step 1: Custom Object Type Definition

Create metadata/custom-objecttype-definitions.xml:

<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://www.demandware.com/xml/impex/metadata/2006-10-31">
<custom-type type-id="BEYABLERanking">
<display-name xml:lang="x-default">BEYABLE Product Ranking</display-name>
<description xml:lang="x-default">Stores BEYABLE product ranking data</description>
<staging-mode>no-staging</staging-mode>
<storage-scope>site</storage-scope>
<key-definition attribute-id="rankingKey">
<type>string</type>
<min-length>1</min-length>
<max-length>256</max-length>
</key-definition>
<attribute-definitions>
<attribute-definition attribute-id="productID">
<display-name xml:lang="x-default">Product ID</display-name>
<description xml:lang="x-default">SFCC Product ID or SKU</description>
<type>string</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>1</min-length>
<max-length>256</max-length>
</attribute-definition>
<attribute-definition attribute-id="position">
<display-name xml:lang="x-default">Ranking Position</display-name>
<description xml:lang="x-default">Position in ranking (1-based)</description>
<type>int</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
<attribute-definition attribute-id="abTestGroup">
<display-name xml:lang="x-default">A/B Test Group</display-name>
<description xml:lang="x-default">A/B test group identifier (optional)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>0</min-length>
<max-length>100</max-length>
</attribute-definition>
<attribute-definition attribute-id="segmentID">
<display-name xml:lang="x-default">Segment ID</display-name>
<description xml:lang="x-default">Visitor segment identifier</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>0</min-length>
<max-length>100</max-length>
</attribute-definition>
<attribute-definition attribute-id="ruleID">
<display-name xml:lang="x-default">Rule ID</display-name>
<description xml:lang="x-default">BEYABLE ranking rule ID</description>
<type>string</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>
<attribute-definition attribute-id="lastSyncDate">
<display-name xml:lang="x-default">Last Sync Date</display-name>
<description xml:lang="x-default">Timestamp of last synchronization</description>
<type>datetime</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>
</attribute-definitions>
<group-definitions>
<attribute-group group-id="BEYABLERankingGroup">
<display-name xml:lang="x-default">BEYABLE Ranking Data</display-name>
<attribute attribute-id="productID"/>
<attribute attribute-id="position"/>
<attribute attribute-id="abTestGroup"/>
<attribute attribute-id="segmentID"/>
<attribute attribute-id="ruleID"/>
<attribute attribute-id="lastSyncDate"/>
</attribute-group>
</group-definitions>
</custom-type>
</metadata>

Changed in v1.1: min-length on the key definition and productID raised from 0 to 1 (a zero-length key is never valid), and max-length added to all bounded string attributes. These missing constraints were the source of the import validation errors reported on v1.0.

Step 2: Site Preferences

Create metadata/site-preferences.xml:

<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://www.demandware.com/xml/impex/metadata/2006-10-31">
<custom-preference-group-definition preference-group-id="BEYABLE">
<display-name xml:lang="x-default">BEYABLE Configuration</display-name>
<description xml:lang="x-default">BEYABLE Product Ranking Configuration</description>
<attribute-definitions>
<!-- General Settings -->
<attribute-definition attribute-id="beyableEnabled">
<display-name xml:lang="x-default">Enable BEYABLE Ranking</display-name>
<description xml:lang="x-default">Enable or disable BEYABLE product ranking</description>
<type>boolean</type>
<mandatory-flag>true</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>

<attribute-definition attribute-id="beyableAccountID">
<display-name xml:lang="x-default">Account ID</display-name>
<description xml:lang="x-default">BEYABLE account ID (32 characters, no hyphens)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>

<attribute-definition attribute-id="beyableSubscriptionKey">
<display-name xml:lang="x-default">Subscription Key</display-name>
<description xml:lang="x-default">BEYABLE API subscription key</description>
<type>password</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>

<attribute-definition attribute-id="beyableAPIBaseURL">
<display-name xml:lang="x-default">API Base URL</display-name>
<description xml:lang="x-default">BEYABLE API base URL</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>https://api.eu1.beyable.com</default-value>
</attribute-definition>

<!-- Ranking Rules -->
<attribute-definition attribute-id="beyableDefaultRankingID">
<display-name xml:lang="x-default">Default Ranking ID</display-name>
<description xml:lang="x-default">Default ranking rule ID (32 characters)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>

<attribute-definition attribute-id="beyableSegmentedRankingID">
<display-name xml:lang="x-default">Segmented Ranking ID</display-name>
<description xml:lang="x-default">Segmented ranking rule ID (optional)</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<min-length>32</min-length>
<max-length>32</max-length>
</attribute-definition>

<attribute-definition attribute-id="beyableUseSegmentation">
<display-name xml:lang="x-default">Use Visitor Segmentation</display-name>
<description xml:lang="x-default">Enable visitor segmentation</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>

<attribute-definition attribute-id="beyableSegmentCookieName">
<display-name xml:lang="x-default">Segment Cookie Name</display-name>
<description xml:lang="x-default">Name of cookie containing visitor segment</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>b_segment</default-value>
</attribute-definition>

<attribute-definition attribute-id="beyableTenantID">
<display-name xml:lang="x-default">Tenant ID</display-name>
<description xml:lang="x-default">BEYABLE tenant identifier for this site</description>
<type>string</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>

<!-- Application Settings -->
<attribute-definition attribute-id="beyableApplyToCategories">
<display-name xml:lang="x-default">Apply to Category Pages</display-name>
<description xml:lang="x-default">Enable ranking on category listing pages</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>

<attribute-definition attribute-id="beyableApplyToSearch">
<display-name xml:lang="x-default">Apply to Search Results</display-name>
<description xml:lang="x-default">Enable ranking on search result pages</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>

<!-- Sync Settings -->
<attribute-definition attribute-id="beyableSyncEnabled">
<display-name xml:lang="x-default">Enable Automatic Sync</display-name>
<description xml:lang="x-default">Enable automatic ranking synchronization</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>true</default-value>
</attribute-definition>

<attribute-definition attribute-id="beyableAPITimeout">
<display-name xml:lang="x-default">API Timeout (seconds)</display-name>
<description xml:lang="x-default">Maximum time to wait for API response</description>
<type>int</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>30</default-value>
</attribute-definition>

<!-- Debug -->
<attribute-definition attribute-id="beyableDebugLogging">
<display-name xml:lang="x-default">Debug Logging</display-name>
<description xml:lang="x-default">Enable debug logging</description>
<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
<default-value>false</default-value>
</attribute-definition>
</attribute-definitions>
</custom-preference-group-definition>
</metadata>

Step 3: Service Configuration

Create metadata/services.xml:

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://www.demandware.com/xml/impex/services/2014-09-26">
<service service-id="beyable.http.ranking">
<enabled-flag>true</enabled-flag>
<type>HTTP</type>
<url>https://api.eu1.beyable.com</url>
<timeout>30000</timeout>
<communication-log>
<type>text</type>
</communication-log>
<mode>production</mode>
<mock-mode-enabled>false</mock-mode-enabled>
</service>
</services>

Metadata Validation

(New in v1.1) Always validate metadata before importing into a live instance.

Pre-Import Checklist

□ XML is well-formed (no unclosed tags, valid nesting)
□ <key-definition> is present in the custom type
□ Bounded string attributes have both <min-length> and <max-length>
□ Key attribute min-length is at least 1
□ Attribute types use valid SFCC values (string, int, boolean, date, datetime, password, ...)
□ No duplicate attribute-id values
□ Job module paths match actual cartridge paths
□ Service ID in services.xml matches the ID used in LocalServiceRegistry.createService()

Import with Validation

  1. Package metadata: zip -r beyable_metadata.zip metadata/
  2. Navigate to Administration > Site Development > Import & Export
  3. Upload the ZIP
  4. Under Meta Data, select the file and click Validate first — do not import directly
  5. Review the validation report:
    • Errors must be fixed before import
    • Warnings should be reviewed
  6. Only after a clean validation, run Import

Common Validation Errors

ErrorCauseFix
Invalid key definitionmin-length of 0 on key, or missing constraintsSet min-length ≥ 1 and add max-length
Attribute constraint missingBounded string without max-lengthAdd <max-length>
Unknown attribute typeTypo or unsupported typeUse a valid SFCC metadata type
Duplicate attributeSame attribute-id declared twiceRename or remove duplicate

Implementation

Step 1: Service Definition

Create cartridge/scripts/services/BEYABLEService.js:

'use strict';

/**
* BEYABLE API Service
*/

var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var Site = require('dw/system/Site');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'BEYABLEService');

/**
* Create BEYABLE HTTP service
* @returns {dw.svc.Service} Service instance
*/
function getService() {
return LocalServiceRegistry.createService('beyable.http.ranking', {
/**
* Create request
* @param {dw.svc.Service} svc Service instance
* @param {Object} params Request parameters
* @returns {string} Request body
*/
createRequest: function (svc, params) {
var currentSite = Site.getCurrent();
var subscriptionKey = currentSite.getCustomPreferenceValue('beyableSubscriptionKey');
var baseURL = currentSite.getCustomPreferenceValue('beyableAPIBaseURL') || 'https://api.eu1.beyable.com';

// Set headers
svc.addHeader('Subscription-Key', subscriptionKey);
svc.addHeader('Accept', 'text/csv');

// Build URL
var url = baseURL + params.endpoint;
svc.setURL(url);
svc.setRequestMethod('GET');

// Log request if debug is enabled
if (currentSite.getCustomPreferenceValue('beyableDebugLogging')) {
Logger.debug('BEYABLE API Request: {0}', url);
}

return null;
},

/**
* Parse response
* @param {dw.svc.Service} svc Service instance
* @param {dw.net.HTTPClient} client HTTP client
* @returns {Object} Parsed response
*/
parseResponse: function (svc, client) {
var currentSite = Site.getCurrent();
var statusCode = client.statusCode;
var responseText = client.text;

if (statusCode !== 200) {
Logger.error('BEYABLE API Error: Status {0}, Response: {1}', statusCode, responseText);
throw new Error('BEYABLE API returned status ' + statusCode);
}

// Parse CSV
var rankings = parseCSV(responseText);

if (currentSite.getCustomPreferenceValue('beyableDebugLogging')) {
Logger.debug('BEYABLE API Response: {0} rankings retrieved', rankings.length);
}

return {
success: true,
rankings: rankings
};
},

/**
* Filter log message
* @param {string} msg Log message
* @returns {string} Filtered message
*/
filterLogMessage: function (msg) {
// Remove sensitive data from logs
return msg.replace(/Subscription-Key: [^\r\n]+/, 'Subscription-Key: [REDACTED]');
}
});
}

/**
* Parse CSV response
* @param {string} csvText CSV content
* @returns {Array} Array of ranking objects
*/
function parseCSV(csvText) {
var rankings = [];
var lines = csvText.split('\n');

// Skip header row
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;

// Parse CSV line (handle quoted values)
var values = parseCSVLine(line);

if (values.length >= 3) {
rankings.push({
productID: values[0],
abTestGroup: values[1] || null,
position: parseInt(values[2], 10)
});
}
}

return rankings;
}

/**
* Parse CSV line handling quoted values
* @param {string} line CSV line
* @returns {Array} Array of values
*/
function parseCSVLine(line) {
var values = [];
var current = '';
var inQuotes = false;

for (var i = 0; i < line.length; i++) {
var char = line.charAt(i);

if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
values.push(current);
current = '';
} else {
current += char;
}
}

values.push(current);
return values;
}

/**
* Fetch rankings from BEYABLE API
* @param {string} ruleID Ranking rule ID
* @param {string} tenantID Tenant ID (optional)
* @param {string} segmentID Segment ID (optional)
* @returns {Object} Service response
*/
function fetchRankings(ruleID, tenantID, segmentID) {
var currentSite = Site.getCurrent();
var accountID = currentSite.getCustomPreferenceValue('beyableAccountID');

if (!accountID || !ruleID) {
throw new Error('BEYABLE account ID and ranking ID are required');
}

// Build endpoint
var endpoint;
if (segmentID && tenantID) {
endpoint = '/segmented-productranking/' + accountID + '/' + ruleID + '/' + segmentID + '/' + tenantID;
} else if (segmentID) {
endpoint = '/segmented-productranking/' + accountID + '/' + ruleID + '/' + segmentID;
} else if (tenantID) {
endpoint = '/productranking/' + accountID + '/' + ruleID + '/' + tenantID;
} else {
endpoint = '/productranking/' + accountID + '/' + ruleID;
}

var service = getService();
var result = service.call({
endpoint: endpoint
});

if (result.status === 'OK') {
return result.object;
} else {
Logger.error('BEYABLE API call failed: {0}', result.errorMessage);
throw new Error('Failed to fetch rankings: ' + result.errorMessage);
}
}

module.exports = {
fetchRankings: fetchRankings,
getService: getService
};

Step 2: Ranking Helper

Create cartridge/scripts/helpers/rankingHelper.js:

'use strict';

/**
* BEYABLE Ranking Helper
*/

var CustomObjectMgr = require('dw/object/CustomObjectMgr');
var Transaction = require('dw/system/Transaction');
var Site = require('dw/system/Site');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'rankingHelper');

/**
* Get ranking key
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @param {string} productID Product ID
* @returns {string} Ranking key
*/
function getRankingKey(ruleID, segmentID, productID) {
var key = ruleID + '-';
if (segmentID) {
key += segmentID + '-';
} else {
key += 'default-';
}
key += productID;
return key;
}

/**
* Store rankings in custom objects
* @param {Array} rankings Array of ranking data
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {number} Number of rankings stored
*/
function storeRankings(rankings, ruleID, segmentID) {
var stored = 0;

Transaction.wrap(function () {
// Clear existing rankings for this rule/segment
clearRankings(ruleID, segmentID);

// Store new rankings
rankings.forEach(function (ranking) {
var key = getRankingKey(ruleID, segmentID, ranking.productID);

try {
var customObj = CustomObjectMgr.createCustomObject('BEYABLERanking', key);
customObj.custom.productID = ranking.productID;
customObj.custom.position = ranking.position;
customObj.custom.abTestGroup = ranking.abTestGroup;
customObj.custom.segmentID = segmentID || null;
customObj.custom.ruleID = ruleID;
customObj.custom.lastSyncDate = new Date();

stored++;
} catch (e) {
Logger.error('Failed to store ranking for product {0}: {1}', ranking.productID, e.message);
}
});
});

Logger.info('Stored {0} rankings for rule {1}, segment {2}', stored, ruleID, segmentID || 'default');
return stored;
}

/**
* Clear rankings for rule/segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
*/
function clearRankings(ruleID, segmentID) {
var rankings;

// Query arguments are passed as varargs after the sort string.
// The {0}/{1} placeholders are bound to these arguments.
if (segmentID) {
rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
null,
ruleID,
segmentID
);
} else {
rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = NULL',
null,
ruleID
);
}

var count = 0;
while (rankings.hasNext()) {
var ranking = rankings.next();
CustomObjectMgr.remove(ranking);
count++;
}

rankings.close();
Logger.debug('Cleared {0} existing rankings', count);
}

/**
* Get ranking for product
* @param {string} productID Product ID
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {dw.object.CustomObject|null} Ranking object
*/
function getRanking(productID, ruleID, segmentID) {
var key = getRankingKey(ruleID, segmentID, productID);
return CustomObjectMgr.getCustomObject('BEYABLERanking', key);
}

/**
* Get all rankings for rule/segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {dw.util.SeekableIterator} Iterator of rankings
*/
function getAllRankings(ruleID, segmentID) {
var sortString = 'custom.position asc';

if (segmentID) {
return CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
sortString,
ruleID,
segmentID
);
}

return CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = NULL',
sortString,
ruleID
);
}

/**
* Apply rankings to product hits
* @param {dw.util.Iterator} productHits Product search hits
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Array} Sorted array of product IDs
*/
function applyRankings(productHits, ruleID, segmentID) {
var currentSite = Site.getCurrent();

if (!currentSite.getCustomPreferenceValue('beyableEnabled')) {
return null;
}

// Get rankings map
var rankingsMap = {};
var rankings = getAllRankings(ruleID, segmentID);

while (rankings.hasNext()) {
var ranking = rankings.next();
rankingsMap[ranking.custom.productID] = ranking.custom.position;
}
rankings.close();

// Create array of products with rankings
var productsWithRankings = [];
var productsWithoutRankings = [];

while (productHits.hasNext()) {
var hit = productHits.next();
var productID = hit.productID;

if (rankingsMap[productID]) {
productsWithRankings.push({
productID: productID,
position: rankingsMap[productID],
hit: hit
});
} else {
productsWithoutRankings.push({
productID: productID,
hit: hit
});
}
}

// Sort ranked products
productsWithRankings.sort(function (a, b) {
return a.position - b.position;
});

// Combine: ranked first, then unranked
var sorted = productsWithRankings.concat(productsWithoutRankings);

return sorted.map(function (item) {
return item.productID;
});
}

module.exports = {
storeRankings: storeRankings,
clearRankings: clearRankings,
getRanking: getRanking,
getAllRankings: getAllRankings,
applyRankings: applyRankings,
getRankingKey: getRankingKey
};

Changed in v1.1: clearRankings() and getAllRankings() now pass the query arguments (ruleID, segmentID) to queryCustomObjects(). In v1.0 the {0}/{1} placeholders were declared but the arguments were never passed, so the queries failed at runtime. This was the main cause of the "referenced methods do not exist" feedback.

Step 3: Segment Helper

Create cartridge/scripts/helpers/beyableHelper.js:

'use strict';

/**
* BEYABLE Helper Functions
*/

var Site = require('dw/system/Site');

/**
* Check if BEYABLE is enabled
* @returns {boolean} True if enabled
*/
function isEnabled() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableEnabled') === true;
}

/**
* Get visitor segment from cookie
* @param {dw.web.Cookies} cookies Request cookies
* @returns {string|null} Segment ID
*/
function getVisitorSegment(cookies) {
var currentSite = Site.getCurrent();

if (!currentSite.getCustomPreferenceValue('beyableUseSegmentation')) {
return null;
}

if (!cookies) {
return null;
}

var cookieName = currentSite.getCustomPreferenceValue('beyableSegmentCookieName') || 'b_segment';
var segmentCookie = cookies[cookieName];

if (segmentCookie && segmentCookie.value) {
return segmentCookie.value;
}

return null;
}

/**
* Get tenant ID for current site
* @returns {string|null} Tenant ID
*/
function getTenantID() {
var currentSite = Site.getCurrent();
var tenantID = currentSite.getCustomPreferenceValue('beyableTenantID');

if (!tenantID) {
// Fallback to site ID
tenantID = currentSite.getID();
}

return tenantID;
}

/**
* Get default ranking ID
* @returns {string|null} Ranking ID
*/
function getDefaultRankingID() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableDefaultRankingID');
}

/**
* Get segmented ranking ID
* @returns {string|null} Segmented ranking ID
*/
function getSegmentedRankingID() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableSegmentedRankingID');
}

/**
* Should apply to categories
* @returns {boolean} True if should apply
*/
function shouldApplyToCategories() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableApplyToCategories') === true;
}

/**
* Should apply to search
* @returns {boolean} True if should apply
*/
function shouldApplyToSearch() {
var currentSite = Site.getCurrent();
return currentSite.getCustomPreferenceValue('beyableApplyToSearch') === true;
}

module.exports = {
isEnabled: isEnabled,
getVisitorSegment: getVisitorSegment,
getTenantID: getTenantID,
getDefaultRankingID: getDefaultRankingID,
getSegmentedRankingID: getSegmentedRankingID,
shouldApplyToCategories: shouldApplyToCategories,
shouldApplyToSearch: shouldApplyToSearch
};

Changed in v1.1: Removed the unused dw/web/Cookie import and added a null guard on cookies. The cookies[cookieName] access pattern itself was already correct in v1.0.

Step 4: Job Script

Create cartridge/scripts/jobs/SyncRankings.js:

'use strict';

/**
* BEYABLE Ranking Sync Job
*
* This job fetches product rankings from the BEYABLE API
* and stores them in SFCC Custom Objects
*/

var Status = require('dw/system/Status');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'SyncRankings');
var Site = require('dw/system/Site');

var BEYABLEService = require('*/cartridge/scripts/services/BEYABLEService');
var rankingHelper = require('*/cartridge/scripts/helpers/rankingHelper');
var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');

/**
* Sync rankings for a specific rule and segment
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Object} Sync result
*/
function syncRankingsForRule(ruleID, segmentID) {
var tenantID = beyableHelper.getTenantID();

Logger.info('Syncing rankings for rule: {0}, segment: {1}, tenant: {2}',
ruleID, segmentID || 'default', tenantID);

try {
// Fetch rankings from API
var response = BEYABLEService.fetchRankings(ruleID, tenantID, segmentID);

if (!response.success) {
throw new Error('API call failed');
}

var rankings = response.rankings;
Logger.info('Fetched {0} rankings from BEYABLE API', rankings.length);

// Optionally filter to active products only (see Performance Optimization)
// rankings = filterActiveProducts(rankings);

// Store rankings
var stored = rankingHelper.storeRankings(rankings, ruleID, segmentID);

return {
success: true,
count: stored
};

} catch (e) {
Logger.error('Error syncing rankings: {0}', e.message);
return {
success: false,
error: e.message
};
}
}

/**
* Main job execution function
* @returns {dw.system.Status} Job status
*/
function execute() {
var currentSite = Site.getCurrent();

Logger.info('Starting BEYABLE ranking synchronization for site: {0}', currentSite.getID());

// Check if BEYABLE is enabled
if (!beyableHelper.isEnabled()) {
Logger.warn('BEYABLE is not enabled for this site');
return new Status(Status.OK, 'DISABLED', 'BEYABLE ranking is not enabled');
}

// Check if sync is enabled
if (!currentSite.getCustomPreferenceValue('beyableSyncEnabled')) {
Logger.warn('BEYABLE sync is disabled');
return new Status(Status.OK, 'DISABLED', 'BEYABLE sync is disabled');
}

var totalSynced = 0;
var errors = [];

try {
// Sync default rankings
var defaultRankingID = beyableHelper.getDefaultRankingID();
if (defaultRankingID) {
var result = syncRankingsForRule(defaultRankingID, null);
if (result.success) {
totalSynced += result.count;
} else {
errors.push('Default ranking: ' + result.error);
}
}

// Sync segmented rankings if enabled
if (currentSite.getCustomPreferenceValue('beyableUseSegmentation')) {
var segmentedRankingID = beyableHelper.getSegmentedRankingID();

if (segmentedRankingID) {
// Note: In production, you might want to sync multiple segments
// For now, we'll sync with a placeholder. Customize based on your segments.
var segments = getSegmentsToSync(); // Implement this based on your needs

segments.forEach(function (segmentID) {
var result = syncRankingsForRule(segmentedRankingID, segmentID);
if (result.success) {
totalSynced += result.count;
} else {
errors.push('Segment ' + segmentID + ': ' + result.error);
}
});
}
}

Logger.info('BEYABLE sync completed. Total rankings synced: {0}', totalSynced);

if (errors.length > 0) {
Logger.error('Sync completed with errors: {0}', errors.join('; '));
return new Status(Status.ERROR, 'ERROR', 'Sync completed with errors: ' + errors.join('; '));
}

return new Status(Status.OK, 'OK', 'Successfully synced ' + totalSynced + ' rankings');

} catch (e) {
Logger.error('Fatal error during sync: {0}', e.message);
return new Status(Status.ERROR, 'ERROR', 'Fatal error: ' + e.message);
}
}

/**
* Get list of segments to sync
* Customize this function based on your segmentation strategy
* @returns {Array} Array of segment IDs
*/
function getSegmentsToSync() {
// Example: Return predefined segments
// In production, you might fetch this from a custom object or site preference
return ['premium', 'standard', 'vip'];
}

module.exports = {
execute: execute
};

Step 5: Product Search Decorator

Create cartridge/models/product/productSearch.js:

'use strict';

var base = module.superModule;

var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');
var rankingHelper = require('*/cartridge/scripts/helpers/rankingHelper');
var Logger = require('dw/system/Logger').getLogger('BEYABLE', 'productSearch');

/**
* Apply BEYABLE ranking to search results
* @param {dw.catalog.ProductSearchModel} productSearch Product search model
* @param {Object} httpParameterMap HTTP parameter map
* @returns {Array} Ranked product IDs
*/
function applyBEYABLERanking(productSearch, httpParameterMap) {
if (!beyableHelper.isEnabled()) {
return null;
}

// Determine if we should apply ranking based on context
var cgid = httpParameterMap.cgid ? httpParameterMap.cgid.stringValue : null;
var searchQuery = httpParameterMap.q ? httpParameterMap.q.stringValue : null;

var shouldApply = false;
if (cgid && beyableHelper.shouldApplyToCategories()) {
shouldApply = true;
} else if (searchQuery && beyableHelper.shouldApplyToSearch()) {
shouldApply = true;
}

if (!shouldApply) {
return null;
}

// Get visitor segment
// Note: `request` is a global variable in SFCC script/controller context.
// Do NOT use require('dw/system/Request').getCurrent() — that method does not exist.
var segmentID = beyableHelper.getVisitorSegment(request.httpCookies);

// Get appropriate ranking ID
var ruleID;
if (segmentID && beyableHelper.getSegmentedRankingID()) {
ruleID = beyableHelper.getSegmentedRankingID();
} else {
ruleID = beyableHelper.getDefaultRankingID();
}

if (!ruleID) {
Logger.warn('No ranking rule ID configured');
return null;
}

// Apply rankings
var productHits = productSearch.getProductSearchHits();
var rankedProductIDs = rankingHelper.applyRankings(productHits, ruleID, segmentID);

return rankedProductIDs;
}

/**
* Extends the base product search model
* @param {dw.catalog.ProductSearchModel} productSearch Product search model
* @param {Object} httpParameterMap HTTP parameter map
* @param {string} sortingRule Sorting rule
* @param {Array} selectedFilters Selected refinements
* @param {dw.catalog.Category} category Category
* @constructor
*/
function ProductSearch(productSearch, httpParameterMap, sortingRule, selectedFilters, category) {
base.call(this, productSearch, httpParameterMap, sortingRule, selectedFilters, category);

// Apply BEYABLE ranking
var rankedIDs = applyBEYABLERanking(productSearch, httpParameterMap);

if (rankedIDs && rankedIDs.length > 0) {
this.beyableRanked = true;

// Re-sort product hits based on ranked IDs.
// getProductSearchHits() returns a fresh iterator on each call,
// so we use the productSearch parameter directly (the base model
// does not expose it on `this`).
var originalHits = productSearch.getProductSearchHits();
var rankedHits = [];
var hitMap = {};

// Create map of product ID to hit
while (originalHits.hasNext()) {
var hit = originalHits.next();
hitMap[hit.productID] = hit;
}

// Reorder hits based on ranking
rankedIDs.forEach(function (productID) {
if (hitMap[productID]) {
rankedHits.push(hitMap[productID]);
}
});

this.productIds = rankedHits.map(function (hit) {
return hit.productID;
});
} else {
this.beyableRanked = false;
}
}

module.exports = ProductSearch;

Changed in v1.1: The global request variable replaces require('dw/system/Request').getCurrent() (which does not exist in the SFCC API), and the hit re-sorting block uses the productSearch parameter instead of this.productSearch (not exposed by the SFRA base model).


Job Configuration

Step 1: Job Definition

Create metadata/jobs.xml:

<?xml version="1.0" encoding="UTF-8"?>
<jobs xmlns="http://www.demandware.com/xml/impex/jobs/2015-07-01">
<job job-id="BEYABLE-SyncRankings">
<description>Synchronize product rankings from BEYABLE API</description>
<is-disabled>false</is-disabled>
<run-mode>parallel</run-mode>
<flow>
<step step-id="SyncRankings" description="Sync BEYABLE product rankings">
<script-module-step>
<module-name>int_beyable_ranking/cartridge/scripts/jobs/SyncRankings.js</module-name>
<function-name>execute</function-name>
<transactional>false</transactional>
<timeout-in-seconds>3600</timeout-in-seconds>
</script-module-step>
</step>
</flow>
<retry-count>3</retry-count>
<retry-delay-in-ms>30000</retry-delay-in-ms>
</job>
</jobs>

Step 2: Schedule Job in Business Manager

  1. Navigate to Administration > Operations > Jobs
  2. Find job BEYABLE-SyncRankings
  3. Click Schedule and Run
  4. Configure schedule:
    • Recurrence: Recurring
    • Interval: Every 2 hours (recommended)
    • Start Time: Choose appropriate time
  5. Click OK

Frontend Integration

Step 1: Tracking Script Template

Create cartridge/templates/default/components/beyable/tracking.isml:

<iscomment>
BEYABLE Tracking Script
This template should be included in the page footer
</iscomment>

<isset name="beyableEnabled" value="${dw.system.Site.current.getCustomPreferenceValue('beyableEnabled')}" scope="page" />

<isif condition="${beyableEnabled}">
<isset name="segmentCookieName" value="${dw.system.Site.current.getCustomPreferenceValue('beyableSegmentCookieName')}" scope="page" />

<script type="text/javascript">
// BEYABLE integration - segment detection
(function() {
var segmentCookieName = '${segmentCookieName}';

// Helper to read cookie
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}

// Log segment for debugging (if debug mode is enabled)
var segment = getCookie(segmentCookieName);
if (segment && console && console.log) {
console.log('BEYABLE Segment:', segment);
}
})();
</script>
</isif>

Modify your footer template to include:

<isinclude template="components/beyable/tracking" />

Testing

Step 1: Configuration Test

  1. Business Manager Configuration:

    • Navigate to Merchant Tools > Site Preferences > Custom Preferences > BEYABLE
    • Enable BEYABLE Ranking
    • Enter Account ID and Subscription Key
    • Configure Ranking IDs
    • Save
  2. Verify Service:

    • Navigate to Administration > Operations > Services
    • Find beyable.http.ranking
    • Click Credentials
    • Verify configuration

Step 2: Manual Job Test

  1. Navigate to Administration > Operations > Jobs
  2. Find BEYABLE-SyncRankings
  3. Click Run
  4. Monitor execution in job logs
  5. Check for errors

Step 3: Verify Data Storage

  1. Navigate to Merchant Tools > Custom Objects > BEYABLERanking
  2. Verify rankings are stored
  3. Check product IDs and positions
  4. Verify last sync date

Step 4: Frontend Test

  1. Clear Cache:

    • Navigate to Administration > Site Development > Cache Management
    • Clear all caches
  2. Test Category Page:

    • Visit a category page on storefront
    • Verify products are ordered according to rankings
    • Use browser dev tools to verify segment cookie
  3. Test Search:

    • Perform a search
    • Verify ranked results

Step 5: Segment Test

  1. Set Test Cookie:

    document.cookie = "b_segment=premium; path=/";
  2. Reload Page:

    • Verify different rankings apply
  3. Clear Cookie:

    document.cookie = "b_segment=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";

Deployment

Production Deployment Checklist

Pre-Deployment

  1. Code Review:

    • Review all custom code
    • Ensure error handling is robust
    • Verify logging is appropriate
  2. Backup:

    • Export site preferences
    • Export custom objects
    • Document current configuration
  3. Prepare Metadata:

    # Package metadata
    zip -r beyable_metadata.zip metadata/

Deployment Steps

  1. Upload Cartridge:

    # Using sgmf-scripts
    npm run upload

    # Or manually via Business Manager
    # Administration > Site Development > Code Deployment
  2. Import Metadata:

    • Navigate to Administration > Site Development > Import & Export
    • Upload beyable_metadata.zip
    • Validate first (see Metadata Validation)
    • Select all components
    • Click Import
  3. Configure Cartridge Path:

    • Navigate to Administration > Sites > Manage Sites > [Your Site]
    • Add int_beyable_ranking to cartridge path
    • Position before app_storefront_base
    • Example: int_beyable_ranking:app_storefront_base
  4. Configure Site Preferences:

    • Navigate to Merchant Tools > Site Preferences > Custom Preferences > BEYABLE
    • Configure all settings
    • Save
  5. Schedule Job:

    • Navigate to Administration > Operations > Jobs
    • Configure BEYABLE-SyncRankings schedule
    • Run initial sync
  6. Clear Caches:

    • Navigate to Administration > Site Development > Cache Management
    • Clear all caches

Post-Deployment

  1. Verify:

    • Check storefront functionality
    • Verify rankings are applied
    • Test segment detection
    • Review logs for errors
  2. Monitor:

    • Monitor job execution logs
    • Check custom object counts
    • Monitor page performance
    • Track error rates
  3. Documentation:

    • Document configuration
    • Create runbook
    • Train support team

Cartridge Maintenance & Updates

(New in v1.1) Since this cartridge is built and maintained by your team (BEYABLE does not currently distribute an official maintained SFCC cartridge), this section defines how to version, update, and roll back the cartridge.

Versioning

Use Semantic Versioning (MAJOR.MINOR.PATCH) in package.json, plus VERSION.txt and CHANGELOG.md at the cartridge root:

Version bumpWhenExample
PATCHBug fix, no behavior change1.1.0 → 1.1.1
MINORNew feature, backward compatible1.1.0 → 1.2.0
MAJORBreaking change (metadata schema change, configuration change, removed behavior)1.x → 2.0.0

Update Procedure

Each update follows the same sequence regardless of size; the depth of testing scales with the version bump:

  1. Branch & implement — develop on a feature/bugfix branch in your Git repository
  2. Update version — bump package.json, VERSION.txt, and add a CHANGELOG.md entry
  3. Test on sandbox — upload cartridge to a sandbox, run the job, verify storefront behavior. For MINOR/MAJOR: full regression on category, search, and segmentation. For MAJOR with metadata changes: validate the metadata import on sandbox first
  4. Backup before deployment — keep the previous cartridge version as a code version in Business Manager (Code Deployment keeps multiple code versions; do not overwrite the active one)
  5. Deploy — upload as a new code version and activate it. This is the key SFCC mechanism: activation is instant, and so is rollback
  6. Re-import metadata — only if metadata changed (MAJOR updates). Validate first
  7. Run initial sync & verify — manual job run, check storefront and logs

Rollback Procedure

SFCC code versions make rollback fast:

Code-only rollback (~5 minutes):

  1. Administration > Site Development > Code Deployment
  2. Activate the previous code version
  3. Clear caches
  4. Verify storefront

Rollback with metadata changes (~1 hour):

  1. Re-activate previous code version (as above)
  2. Re-import the previous metadata export (this is why pre-deployment backup of site preferences and custom object definitions matters)
  3. Run a full re-sync to repopulate custom objects in the previous schema
  4. Clear caches and verify

Release Notes

For each release, record in CHANGELOG.md: Added / Changed / Fixed / Breaking Changes, plus a migration note for any MAJOR release. If multiple teams or sites consume the cartridge, distribute release notes before deployment with: the version, the change summary, whether metadata re-import is required, and the rollback plan.


Monitoring & Troubleshooting

Log Files

Monitor these log files:

  1. Custom Logs (BEYABLE-*.log):

    • BEYABLE-BEYABLEService.log
    • BEYABLE-rankingHelper.log
    • BEYABLE-SyncRankings.log
  2. System Logs:

    • customdebug.log
    • customerror.log
    • customwarn.log

Common Issues

1. API Authentication Failures

Symptom: HTTP 401 errors in service logs

Solution:

  • Verify Subscription-Key in Business Manager
  • Test API manually:
    curl -H "Subscription-Key: YOUR_KEY" \
    "https://api.eu1.beyable.com/productranking/ACCOUNT_ID/RANKING_ID/TENANT"
  • Check service configuration in Business Manager

2. Rankings Not Applied

Symptom: Products not sorted by BEYABLE ranking

Solution:

  1. Verify BEYABLE is enabled in site preferences
  2. Check custom object count:
    • Navigate to Merchant Tools > Custom Objects > BEYABLERanking
  3. Verify job execution:
    • Administration > Operations > Jobs > Job History
  4. Enable debug logging
  5. Clear caches

3. Job Failures

Symptom: Sync job shows error status

Solution:

  1. Review job execution log
  2. Check service call logs
  3. Verify API credentials
  4. Test service manually in Business Manager
  5. Check for quota limits

4. Performance Issues

Symptom: Slow category/search pages

Solution:

  • Enable page caching for category pages
  • Index custom objects
  • Reduce ranking object count (filter by active products)
  • Consider CDN for static assets

5. Segment Not Detected

Symptom: Segmented rankings not working

Solution:

  1. Verify cookie name matches configuration
  2. Check browser cookies in dev tools
  3. Test with manual cookie
  4. Verify BEYABLE tracking is loaded
  5. Check cookie domain settings

Debug Commands

Test Service Connection:

Create a test controller cartridge/controllers/BEYABLERanking.js:

'use strict';

var server = require('server');
var BEYABLEService = require('*/cartridge/scripts/services/BEYABLEService');
var beyableHelper = require('*/cartridge/scripts/helpers/beyableHelper');

server.get('TestConnection', function (req, res, next) {
var ruleID = beyableHelper.getDefaultRankingID();
var tenantID = beyableHelper.getTenantID();

try {
var result = BEYABLEService.fetchRankings(ruleID, tenantID, null);

res.json({
success: true,
count: result.rankings.length,
sample: result.rankings.slice(0, 5)
});
} catch (e) {
res.json({
success: false,
error: e.message
});
}

next();
});

module.exports = server.exports();

Access: https://your-site.com/on/demandware.store/Sites-YourSite-Site/en_US/BEYABLERanking-TestConnection

Security note: Restrict or remove this endpoint in production (e.g. guard with server.middleware.https and an allowlist, or only deploy it on sandbox/staging code versions).


Performance Optimization

1. Custom Object Indexing

While SFCC doesn't support custom indexes on custom objects, optimize queries:

// Use specific queries instead of iterating all objects
var rankings = CustomObjectMgr.queryCustomObjects(
'BEYABLERanking',
'custom.ruleID = {0} AND custom.segmentID = {1}',
'custom.position asc',
ruleID,
segmentID
);

2. Caching Strategy

Implement application-level caching using the custom cache registered in caches.json (see Installation Step 4):

var CacheMgr = require('dw/system/CacheMgr');

/**
* Get rankings with caching
* Note: cache plain arrays, not SeekableIterators — iterators are
* stateful objects and cannot be stored in the cache.
* @param {string} ruleID Rule ID
* @param {string} segmentID Segment ID (optional)
* @returns {Array} Array of {productID, position} entries
*/
function getCachedRankings(ruleID, segmentID) {
var cache = CacheMgr.getCache('BEYABLERankings');
var cacheKey = ruleID + '-' + (segmentID || 'default');

return cache.get(cacheKey, function () {
// Loader function: executed only on cache miss
var result = [];
var rankings = getAllRankings(ruleID, segmentID);

while (rankings.hasNext()) {
var ranking = rankings.next();
result.push({
productID: ranking.custom.productID,
position: ranking.custom.position
});
}
rankings.close();

return result;
});
}

Changed in v1.1: v1.0 cached the SeekableIterator returned by getAllRankings() directly and used cache.put() with a TTL argument. Iterators cannot be cached (they are consumed on first read), and entry TTL is defined in caches.json (expireAfterSeconds), not per put() call. The loader-function form of cache.get() is the recommended SFCC pattern.

3. Page Caching

Enable page caching for category pages in cacheable.isml:

<iscache type="relative" hour="1" varyby="price_promotion"/>

4. Reduce Object Count

Only sync rankings for active, online products:

// In SyncRankings.js
function filterActiveProducts(rankings) {
var ProductMgr = require('dw/catalog/ProductMgr');

return rankings.filter(function(ranking) {
var product = ProductMgr.getProduct(ranking.productID);
return product && product.online;
});
}

5. Async Job Processing

For very large catalogs, consider splitting the sync job:

<!-- Split by product category or segment -->
<job job-id="BEYABLE-SyncRankings-CategoryA">
<!-- Sync only category A -->
</job>

<job job-id="BEYABLE-SyncRankings-CategoryB">
<!-- Sync only category B -->
</job>

Appendix

A. API Endpoint Reference

EndpointUse Case
/productranking/{accountId}/{rankingId}Default tenant, no segmentation
/productranking/{accountId}/{rankingId}/{tenant}Specific tenant, no segmentation
/segmented-productranking/{accountId}/{rankingId}/{segmentId}Default tenant, with segmentation
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant}Full configuration

B. Business Manager Navigation

TaskNavigation Path
Configure BEYABLEMerchant Tools > Site Preferences > Custom Preferences > BEYABLE
Manage Custom ObjectsMerchant Tools > Custom Objects > BEYABLERanking
View Job LogsAdministration > Operations > Jobs > Job History
Manage ServicesAdministration > Operations > Services > beyable.http.ranking
Deploy CodeAdministration > Site Development > Code Deployment
Import MetadataAdministration > Site Development > Import & Export
Clear CacheAdministration > Site Development > Cache Management

C. Troubleshooting Checklist

  • BEYABLE enabled in site preferences
  • Account ID and Subscription Key configured
  • Ranking rule IDs configured
  • Cartridge added to cartridge path
  • caches.json present and registered in package.json
  • Metadata validated and imported successfully
  • Job scheduled and running
  • Custom objects created
  • Rankings applied to product listings
  • Logs free of errors
  • Segment cookie detected (if using segmentation)

Support

For additional assistance:

  • Cartridge Issues: Check SFCC logs and error messages
  • BEYABLE API Issues: Contact your BEYABLE account manager
  • SFCC Issues: Refer to SFCC documentation or contact Salesforce support

Changelog

VersionDateChanges
1.1.02026-06-04Fixed queryCustomObjects() calls (query arguments were never bound), replaced non-existent Request.getCurrent() with the global request, fixed getCachedRankings() (cache arrays via loader function, register cache in caches.json), corrected metadata length constraints causing import validation errors, fixed decorator to use the productSearch parameter. Added Metadata Validation and Cartridge Maintenance & Updates sections.
1.0.02026-02-26Initial SFCC integration guide