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. Implementation
  7. Job Configuration
  8. Frontend Integration
  9. Testing
  10. Deployment
  11. Monitoring & Troubleshooting
  12. 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
└── 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

  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
    • 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

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


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

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
  • 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

VersionDateChanges
1.0.02026-02-26Initial SFCC integration guide