AI-Powered Product Recommendations in Magento 2
Personalization isn't optional anymore. Shoppers expect it. Stores that deliver it see measurably higher conversion rates.
Most tutorials on AI product recommendations for Magento 2 stop at "install this extension and configure it." That's not helpful if you're building something custom, integrating with a specific data model, or trying to understand what's actually happening under the hood.
This guide goes further. You'll build a working Magento 2 module that integrates with Adobe's Product Recommendations service — powered by Adobe Sensei, Adobe's AI engine. It processes recommendation payloads and renders personalized product suggestions on the frontend, with real PHP and XML you can use.
✓ What you'll learn: - Build a Magento 2 module from scratch - Authenticate with Adobe's API using OAuth tokens - Fetch and render AI-powered recommendations - Cache results and degrade gracefully on failure
---
What You'll Need
Prerequisites
- Adobe Commerce 2.4.6+ — Cloud or on-premise, with valid license - Basic familiarity with Magento 2 module structure, dependency injection, and layout XML
Required Access
- Adobe Product Recommendations API access — Available through Adobe Developer Console under your IMS Organization. Think of your IMS Organization as the account umbrella for all your Adobe products. - OAuth Server-to-Server credentials — OAuth tokens work like temporary API passwords. They expire after an hour. Adobe deprecated the older JWT approach in 2024 (see Adobe's migration guide).
Environment Setup
- PHP 8.2+ with extensions: curl, json, mbstring
- Composer 2.x and working CLI access to your Magento root
- A staging environment — Never prototype API integrations on production
---
Why AI Recommendations Matter for Adobe Commerce Stores
Rule-based engines are outdated. Simple "customers also bought" logic ignores session context, browsing history, inventory changes, and seasonal demand. The results feel generic — because they are.
Adobe's Product Recommendations service addresses this directly. It applies machine learning models trained on behavioral data — clickstreams, purchase patterns, dwell time — across Commerce Cloud instances. The models improve continuously.
The practical difference? A static rule surfaces a phone case when someone views a phone. Adobe Sensei surfaces the right phone case. It matches the buyer's price sensitivity, browsing pattern, and device — within milliseconds.
The out-of-the-box magento/product-recommendations package works well for standard storefronts. Custom themes, headless front-ends, and custom scoring logic require direct API integration. That's what this tutorial covers.
Now that you understand why AI recommendations matter, let's get your API credentials set up.
Note: This tutorial demonstrates direct API integration patterns. If you're using the official
magento/product-recommendationspackage, consult the Adobe Commerce Product Recommendations documentation for the standard SaaS connector approach.
---
Setting Up API Credentials and Module Scaffold
Getting Your Credentials
You'll need three things from Adobe Developer Console. Here's how to get them:
You'll receive four values:
- client_id
- client_secret
- OAuth scopes specific to your project
- Your organization_id from Commerce SaaS configuration
Store these in Magento's encrypted config. Never put them in plain env.php.
Scaffolding the Module
mkdir -p app/code/YourVendor/SenseiRecommendations/{Api,Block,Controller,Model,ViewModel}
Replace
YourVendorwith your actual vendor namespace. A unique vendor prefix prevents conflicts in shared environments.
Your registration.php:
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'YourVendor_SenseiRecommendations',
__DIR__
);
Your composer.json declares PHP and Magento version constraints:
{
"require": {
"php": ">=8.2",
"magento/framework": ">=107.0"
}
}
Adding Admin Configuration
Add etc/adminhtml/system.xml so store admins can manage credentials through the UI:
<group id="api" translate="label" type="text" sortOrder="10" showInDefault="1">
<label>Product Recommendations API</label>
<field id="client_id" translate="label" type="text" sortOrder="10" showInDefault="1">
<label>Client ID</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
<field id="client_secret" translate="label" type="obscure" sortOrder="20" showInDefault="1">
<label>Client Secret</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
</group>
Pro Tip: Always use
Magento\Config\Model\Config\Backend\Encryptedfor API credentials. Magento encrypts values at rest using the deployment key. Never use plaintexttype fields for secrets.
---
Building the Recommendation Engine
Your module needs two core services. First, an authentication service to manage OAuth tokens. Second, a fetcher to retrieve recommendations. Let's build both.
The Authentication Service
Adobe's API uses OAuth 2.0 bearer tokens. Tokens expire, so your service must handle refresh logic. AuthTokenService manages this lifecycle:
<?php
declare(strict_types=1);
namespace YourVendor\SenseiRecommendations\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\HTTP\Client\Curl;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Store\Model\ScopeInterface;
use Psr\Log\LoggerInterface;
class AuthTokenService
{
private const TOKEN_URL = 'https://ims-na1.adobelogin.com/ims/token/v3';
private ?string $cachedToken = null;
private int $tokenExpiry = 0;
public function __construct(
private readonly ScopeConfigInterface $scopeConfig,
private readonly Curl $curl,
private readonly SerializerInterface $serializer,
private readonly LoggerInterface $logger
) {}
public function getToken(): string
{
if ($this->cachedToken && time() < $this->tokenExpiry - 60) {
return $this->cachedToken;
}
$clientId = $this->scopeConfig->getValue(
'sensei_recommendations/api/client_id',
ScopeInterface::SCOPE_STORE
);
$clientSecret = $this->scopeConfig->getValue(
'sensei_recommendations/api/client_secret',
ScopeInterface::SCOPE_STORE
);
$this->curl->setHeaders(['Content-Type' => 'application/x-www-form-urlencoded']);
$this->curl->post(self::TOKEN_URL, http_build_query([
'grant_type' => 'client_credentials',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'scope' => 'your_project_specific_scopes', // Use scopes from Developer Console
]));
$response = $this->serializer->unserialize($this->curl->getBody());
if (empty($response['access_token'])) {
$this->logger->error('Product Recommendations auth failed', ['response' => $response]);
throw new \RuntimeException('Failed to obtain access token.');
}
$this->cachedToken = $response['access_token'];
$this->tokenExpiry = time() + (int)($response['expires_in'] ?? 3600);
return $this->cachedToken;
}
public function clearToken(): void
{
$this->cachedToken = null;
$this->tokenExpiry = 0;
}
}
The 60-second buffer on tokenExpiry prevents race conditions where a token expires mid-request. clearToken() forces a refresh after a 401 response.
The Recommendation Fetcher
Authentication is handled. Now RecommendationFetcher retrieves results and degrades gracefully on failure:
public function fetch(string $customerId, string $currentSku, int $limit = 6): array
{
$maxRetries = 2;
$attempt = 0;
while ($attempt <= $maxRetries) {
try {
$token = $this->authTokenService->getToken();
$start = microtime(true);
$this->curl->setHeaders([
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'x-api-key' => $this->getClientId(),
]);
$this->curl->post($this->getEndpointUrl(), $this->serializer->serialize([
'context' => ['customerId' => $customerId ?: 'anonymous', 'sku' => $currentSku],
'limit' => $limit,
]));
$status = $this->curl->getStatus();
$this->logger->debug('Recommendations API', [
'status' => $status,
'ms' => round((microtime(true) - $start) * 1000),
]);
if ($status === 401) {
$this->authTokenService->clearToken();
$attempt++;
continue;
}
$response = $this->serializer->unserialize($this->curl->getBody());
return $response['recommendations'] ?? [];
} catch (\Throwable $e) {
$this->logger->warning('Recommendation fetch failed: ' . $e->getMessage());
return [];
}
}
return [];
}
Note: Replace
$this->getEndpointUrl()with your actual SaaS endpoint from Adobe Commerce SaaS configuration. The retry loop handles 401 token expiry — other errors fail fast and return an empty array.
Warning: Returning
[]on failure is intentional. Recommendation failures must degrade gracefully. Never let a third-party API error break your product page.
---
Rendering Results in Frontend Blocks
Data is flowing in. Now surface it to shoppers. Here's the approach:
- A view model retrieves the current product, fetches SKUs, and loads product objects. Keep logic out of block classes.
- Layout XML attaches the view model to content.bottom on product pages via catalog_product_view.xml.
- A template renders a minimal . Style it with your theme's component library.
Inject RecommendationFetcher into the view model directly — not into the block. This follows the same patterns established by the services above and keeps your architecture consistent.
---
Testing, Caching, and Performance
Unit Testing
Mock both AuthTokenService and Curl to test fetching logic in isolation:
public function testFetchReturnsEmptyArrayOnApiFailure(): void
{
$this->authTokenServiceMock->method('getToken')
->willThrowException(new \RuntimeException('Auth failed'));
$result = $this->fetcher->fetch('123', 'SKU-001');
$this->assertSame([], $result);
}
Always test the failure path first. The happy path is easy — degraded states break production.
Caching
Live API calls on every product page view won't scale. Cache results with a 5-minute TTL:
$cacheKey = 'sensei_rec_' . md5($customerId . $currentSku);
$cached = $this->cache->load($cacheKey);
if ($cached) {
return $this->serializer->unserialize($cached);
}
$results = $this->fetchFromApi($customerId, $currentSku);
$this->cache->save($this->serializer->serialize($results), $cacheKey, ['sensei_recommendations'], 300);
Tag entries with ['sensei_recommendations'] to flush them programmatically — for example, when inventory changes via an observer.
Asynchronous Loading
Don't block page render on the API response. Render an empty container with data-sku and data-customer-id attributes, then populate it via XHR from a dedicated controller endpoint. This keeps Core Web Vitals clean and isolates API latency from the initial page load.
---
Building a real Product Recommendations integration isn't a one-afternoon job. But it's not the black box most tutorials make it out to be. The architecture here — auth service, fetcher, view model, cached results — follows Magento's own patterns and extends cleanly. You could swap the underlying ML endpoint tomorrow without touching templates or layout XML.
Three things to carry forward: degrade gracefully when the API fails, cache aggressively with targeted invalidation, and keep data logic separate from presentation. Get those right. You'll have a foundation that holds up in production.

