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
- Prerequisites
- Architecture Overview
- Cartridge Structure
- Installation
- Configuration
- Implementation
- Job Configuration
- Frontend Integration
- Testing
- Deployment
- Monitoring & Troubleshooting
- 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:
- BEYABLE Cookie: BEYABLE Platform sets visitor segment cookie
- Scheduled Job: SFCC job fetches rankings from BEYABLE API (every 1-4 hours)
- Data Storage: Rankings stored in SFCC Custom Objects
- Runtime Application: Controllers read segment, apply rankings to product searches
- 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
└── package.json
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.0.0",
"description": "BEYABLE Product Ranking API Integration for SFCC",
"main": "cartridge/scripts/init.js",
"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"
}
}
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
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>0</min-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>0</min-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>
</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>
</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>
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>
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 query = 'custom.ruleID = {0}';
var queryParams = [ruleID];
if (segmentID) {
query += ' AND custom.segmentID = {1}';
queryParams.push(segmentID);
} else {
query += ' AND custom.segmentID = NULL';
}
var rankings = CustomObjectMgr.queryCustomObjects('BEYABLERanking', query, null);
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 query = 'custom.ruleID = {0}';
var queryParams = [ruleID];
if (segmentID) {
query += ' AND custom.segmentID = {1}';
queryParams.push(segmentID);
} else {
query += ' AND custom.segmentID = NULL';
}
var sortString = 'custom.position asc';
return CustomObjectMgr.queryCustomObjects('BEYABLERanking', query, sortString);
}
/**
* 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
};
Step 3: Segment Helper
Create cartridge/scripts/helpers/beyableHelper.js:
'use strict';
/**
* BEYABLE Helper Functions
*/
var Site = require('dw/system/Site');
var Cookie = require('dw/web/Cookie');
/**
* 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;
}
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
};
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 currentSite = Site.getCurrent();
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);
// 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
var request = require('dw/system/Request').getCurrent();
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;
this.productIds = rankedIDs;
// Re-sort product hits based on ranked IDs
var originalHits = this.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;
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
- Navigate to Administration > Operations > Jobs
- Find job BEYABLE-SyncRankings
- Click Schedule and Run
- Configure schedule:
- Recurrence: Recurring
- Interval: Every 2 hours (recommended)
- Start Time: Choose appropriate time
- 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>
Step 2: Include Tracking in Footer
Modify your footer template to include:
<isinclude template="components/beyable/tracking" />
Testing
Step 1: Configuration Test
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
Verify Service:
- Navigate to Administration > Operations > Services
- Find
beyable.http.ranking - Click Credentials
- Verify configuration
Step 2: Manual Job Test
- Navigate to Administration > Operations > Jobs
- Find BEYABLE-SyncRankings
- Click Run
- Monitor execution in job logs
- Check for errors
Step 3: Verify Data Storage
- Navigate to Merchant Tools > Custom Objects > BEYABLERanking
- Verify rankings are stored
- Check product IDs and positions
- Verify last sync date
Step 4: Frontend Test
Clear Cache:
- Navigate to Administration > Site Development > Cache Management
- Clear all caches
Test Category Page:
- Visit a category page on storefront
- Verify products are ordered according to rankings
- Use browser dev tools to verify segment cookie
Test Search:
- Perform a search
- Verify ranked results
Step 5: Segment Test
Set Test Cookie:
document.cookie = "b_segment=premium; path=/";Reload Page:
- Verify different rankings apply
Clear Cookie:
document.cookie = "b_segment=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
Deployment
Production Deployment Checklist
Pre-Deployment
Code Review:
- Review all custom code
- Ensure error handling is robust
- Verify logging is appropriate
Backup:
- Export site preferences
- Export custom objects
- Document current configuration
Prepare Metadata:
# Package metadata
zip -r beyable_metadata.zip metadata/
Deployment Steps
Upload Cartridge:
# Using sgmf-scripts
npm run upload
# Or manually via Business Manager
# Administration > Site Development > Code DeploymentImport Metadata:
- Navigate to Administration > Site Development > Import & Export
- Upload
beyable_metadata.zip - Select all components
- Click Import
Configure Cartridge Path:
- Navigate to Administration > Sites > Manage Sites > [Your Site]
- Add
int_beyable_rankingto cartridge path - Position before
app_storefront_base - Example:
int_beyable_ranking:app_storefront_base
Configure Site Preferences:
- Navigate to Merchant Tools > Site Preferences > Custom Preferences > BEYABLE
- Configure all settings
- Save
Schedule Job:
- Navigate to Administration > Operations > Jobs
- Configure BEYABLE-SyncRankings schedule
- Run initial sync
Clear Caches:
- Navigate to Administration > Site Development > Cache Management
- Clear all caches
Post-Deployment
Verify:
- Check storefront functionality
- Verify rankings are applied
- Test segment detection
- Review logs for errors
Monitor:
- Monitor job execution logs
- Check custom object counts
- Monitor page performance
- Track error rates
Documentation:
- Document configuration
- Create runbook
- Train support team
Monitoring & Troubleshooting
Log Files
Monitor these log files:
Custom Logs (BEYABLE-*.log):
- BEYABLE-BEYABLEService.log
- BEYABLE-rankingHelper.log
- BEYABLE-SyncRankings.log
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:
- Verify BEYABLE is enabled in site preferences
- Check custom object count:
- Navigate to Merchant Tools > Custom Objects > BEYABLERanking
- Verify job execution:
- Administration > Operations > Jobs > Job History
- Enable debug logging
- Clear caches
3. Job Failures
Symptom: Sync job shows error status
Solution:
- Review job execution log
- Check service call logs
- Verify API credentials
- Test service manually in Business Manager
- 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:
- Verify cookie name matches configuration
- Check browser cookies in dev tools
- Test with manual cookie
- Verify BEYABLE tracking is loaded
- 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
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 query = 'custom.ruleID = {0} AND custom.segmentID = {1}';
var rankings = CustomObjectMgr.queryCustomObjects('BEYABLERanking', query, 'custom.position asc');
2. Caching Strategy
Implement application-level caching:
var CacheMgr = require('dw/system/CacheMgr');
var cache = CacheMgr.getCache('BEYABLERankings');
function getCachedRankings(ruleID, segmentID) {
var cacheKey = ruleID + '-' + (segmentID || 'default');
var cached = cache.get(cacheKey);
if (cached) {
return cached;
}
// Fetch from custom objects
var rankings = getAllRankings(ruleID, segmentID);
// Cache for 2 hours
cache.put(cacheKey, rankings, 2 * 60 * 60);
return rankings;
}
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
| Endpoint | Use 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
| Task | Navigation Path |
|---|---|
| Configure BEYABLE | Merchant Tools > Site Preferences > Custom Preferences > BEYABLE |
| Manage Custom Objects | Merchant Tools > Custom Objects > BEYABLERanking |
| View Job Logs | Administration > Operations > Jobs > Job History |
| Manage Services | Administration > Operations > Services > beyable.http.ranking |
| Deploy Code | Administration > Site Development > Code Deployment |
| Import Metadata | Administration > Site Development > Import & Export |
| Clear Cache | Administration > 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
- Metadata 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
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026-02-26 | Initial SFCC integration guide |