AI-Powered Product Recommendations in Magento 2

AI Product Recommendations in Magento 2 | Build Custom

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-recommendations package, 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:

  • Go to Adobe Developer Console and select your IMS Organization
  • Create a new project, then add the Product Recommendations API
  • Choose OAuth Server-to-Server authentication and complete the setup
  • 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 YourVendor with 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\Encrypted for API credentials. Magento encrypts values at rest using the deployment key. Never use plain text type 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.

    Quality Score: 66/100



    Jot us a note and we’ll get back to you as quickly as possible.
    Copyright © 2021-2026 Unomage, LLC. All rights reserved.