← Retour aux articles

Shopware 6 : Pixel et CAPI OpenAI Ads pour Shopware 6 : Configuration complète

Guide technique étape par étape pour intégrer le pixel et l'API Conversions OpenAI Ads sur Shopware 6 avec Twig et EventSubscribers Symfony.

2026-03-24 • eCommerce Platforms

Shopware 6 is a powerhouse for enterprise eCommerce across Europe, but tracking paid conversions on it can be an absolute minefield. Between Germany's strict cookie consent enforcement (TTDSG/GDPR), Safari's 24-hour ITP caps, and aggressive uBlock extensions, standard browser pixels routinely drop 18% to 35% of order telemetry before it ever reaches OpenAI Ads Manager.

If you rely solely on JavaScript fired in your Twig templates, you are flying blind.

The fix is a unified hybrid telemetry model. A lightweight Twig template handles client-side page views and add-to-cart clicks, while a dedicated Symfony EventSubscriber captures CheckoutOrderPlacedEvent on the backend to fire orders directly through the Conversions API (CAPI). Tie them together with a deterministic event_id, and OpenAI will deduplicate browser and server hits automatically.

No dropped orders. No double counts.

Browser Interaction (Catalog, Cart)
       │
       ▼
[Shopware 6 Twig Theme] ──(bzrcdn.openai.com)──► Client Pixel (Pageview, ViewContent, AddToCart)
       │
       ▼ Checkout Order Placed
[Shopware Core EventDispatcher]
       │
       ▼ CheckoutOrderPlacedEvent
[OpenAiTrackingSubscriber.php]
       │
       ▼ cURL / Guzzle POST (event_id: sw6_order_10024)
OpenAI Ads Conversions API (https://bzr.openai.com/v1/events)

Part 1: Storefront Pixel Integration via Twig Extension

Avoid modifying vendor code directly. Instead, extend your theme or custom plugin inside custom/plugins/ComercioOpenAiAds/src/Resources/views/storefront/layout/meta.html.twig.

Step 1: Inject Base Pixel Snippet

{% sw_extends '@Storefront/storefront/layout/meta.html.twig' %}

{% block layout_head_meta_tags %}
    {{ parent() }}
    
    <script>
      !function(w,d,e,u,t,s){if(w.openaiAds)return;t=w.openaiAds=function(){
      t.callMethod?t.callMethod.apply(t,arguments):t.queue.push(arguments)};
      if(!w._openaiAds)w._openaiAds=t;t.push=t;t.loaded=!0;t.version='1.0';
      t.queue=[];s=d.createElement(e);s.async=!0;s.src=u;
      var f=d.getElementsByTagName(e)[0];f.parentNode.insertBefore(s,f);
      }(window,document,'script','https://bzrcdn.openai.com/pixel.js');

      openaiAds('init', '{{ config('ComercioOpenAiAds.config.pixelId') }}');
      openaiAds('track', 'PageView');
    </script>
    <noscript>
      <img height="1" width="1" style="display:none" 
           src="https://bzr.openai.com/v1/events?pixel_id={{ config('ComercioOpenAiAds.config.pixelId') }}&event=PageView&noscript=1" />
    </noscript>
{% endblock %}

Step 2: Track Storefront Product Views and AddToCart

Extend storefront/page/product-detail/index.html.twig to fire catalog interaction events:

{% sw_extends '@Storefront/storefront/page/product-detail/index.html.twig' %}

{% block page_product_detail_main_container %}
    {{ parent() }}
    <script>
      document.addEventListener('DOMContentLoaded', function () {
        if (window.openaiAds) {
          window.openaiAds('track', 'ViewContent', {
            content_type: 'product',
            content_ids: ['{{ page.product.productNumber }}'],
            content_name: '{{ page.product.translated.name|e('js') }}',
            value: Math.round({{ page.product.calculatedPrice.unitPrice }} * 100),
            currency: '{{ context.currency.isoCode }}'
          });
        }
      });
    </script>
{% endblock %}

Part 2: Server-Side CAPI Implementation via EventSubscriber

The server-side integration captures the completed checkout directly from the Shopware 6 application layer, bypassing client network restrictions.

Step 1: Create the Checkout Subscriber

Create src/Subscriber/CheckoutOrderPlacedSubscriber.php:

<?php declare(strict_types=1);

namespace Comercio\OpenAiAds\Subscriber;

use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class CheckoutOrderPlacedSubscriber implements EventSubscriberInterface
{
    private SystemConfigService $configService;
    private HttpClientInterface $httpClient;

    public function __construct(SystemConfigService $configService, HttpClientInterface $httpClient)
    {
        $this->configService = $configService;
        $this->httpClient = $httpClient;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            CheckoutOrderPlacedEvent::class => 'onOrderPlaced',
        ];
    }

    public function onOrderPlaced(CheckoutOrderPlacedEvent $event): void
    {
        $order = $event->getOrder();
        $orderCustomer = $order->getOrderCustomer();
        if (!$orderCustomer) {
            return;
        }

        $pixelId = $this->configService->get('ComercioOpenAiAds.config.pixelId');
        $apiToken = $this->configService->get('ComercioOpenAiAds.config.apiToken');
        if (!$pixelId || !$apiToken) {
            return;
        }

        $orderNumber = $order->getOrderNumber();
        $eventId = 'sw6_order_' . $orderNumber;

        // Extract and normalize customer data
        $email = strtolower(trim($orderCustomer->getEmail()));
        $hashedEmail = hash('sha256', $email);

        $billingAddress = $order->getBillingAddress();
        $hashedPhone = null;
        if ($billingAddress && $billingAddress->getPhoneNumber()) {
            $rawPhone = preg_replace('/[^0-9]/', '', $billingAddress->getPhoneNumber());
            $hashedPhone = hash('sha256', $rawPhone);
        }

        // Amount in minor currency units (cents)
        $amountInMinorUnits = (int) round($order->getPrice()->getTotalPrice() * 100);
        $currencyCode = $event->getContext()->getCurrency()->getIsoCode();

        $contentIds = [];
        if ($order->getLineItems()) {
            foreach ($order->getLineItems() as $item) {
                if ($item->getPayload() && isset($item->getPayload()['productNumber'])) {
                    $contentIds[] = (string) $item->getPayload()['productNumber'];
                }
            }
        }

        $payload = [
            'data' => [
                [
                    'event_name' => 'Purchase',
                    'event_time' => time(),
                    'event_id' => $eventId,
                    'user_data' => array_filter([
                        'em' => $hashedEmail,
                        'ph' => $hashedPhone,
                        'country' => $billingAddress && $billingAddress->getCountry() ? hash('sha256', strtolower($billingAddress->getCountry()->getIso())) : null,
                        'client_user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
                    ]),
                    'custom_data' => [
                        'currency' => $currencyCode,
                        'value' => $amountInMinorUnits,
                        'order_id' => $orderNumber,
                        'content_ids' => $contentIds,
                        'content_type' => 'product',
                    ],
                    'action_source' => 'system_generated'
                ]
            ]
        ];

        try {
            $this->httpClient->request('POST', 'https://bzr.openai.com/v1/events', [
                'headers' => [
                    'Authorization' => 'Bearer ' . $apiToken,
                    'Content-Type' => 'application/json',
                ],
                'json' => $payload,
                'timeout' => 3.5,
            ]);
        } catch (\Throwable $e) {
            // Log failure to Shopware Monolog channel
        }
    }
}

Step 2: Register Subscriber in services.xml

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="Comercio\OpenAiAds\Subscriber\CheckoutOrderPlacedSubscriber">
            <argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
            <argument type="service" id="Symfony\Contracts\HttpClient\HttpClientInterface"/>
            <tag name="kernel.event_subscriber"/>
        </service>
    </services>
</container>

Part 3: Browser Purchase Deduplication on finish.html.twig

To allow the browser to report client cookies while preventing duplicate reporting with the backend subscriber, output the identical event_id on the order finish page:

{% sw_extends '@Storefront/storefront/page/checkout/finish/index.html.twig' %}

{% block page_checkout_finish %}
    {{ parent() }}
    {% if page.order %}
    <script>
      document.addEventListener('DOMContentLoaded', function () {
        if (window.openaiAds) {
          window.openaiAds('track', 'Purchase', {
            currency: '{{ page.order.currency.isoCode }}',
            value: Math.round({{ page.order.price.totalPrice }} * 100),
            order_id: '{{ page.order.orderNumber }}',
            content_type: 'product'
          }, {
            event_id: 'sw6_order_{{ page.order.orderNumber }}'
          });
        }
      });
    </script>
    {% endif %}
{% endblock %}

Verification & QA Checklist

  1. Pixel Loading: Inspect the browser console network tab for requests to https://bzrcdn.openai.com/pixel.js returning HTTP 200.
  2. Event Payload: Verify that Purchase events contain value in cents (e.g. 12900 for €129.00) and proper uppercase currency codes (EUR, USD).
  3. Deduplication Matching: Ensure the event_id sent in finish.html.twig matches sw6_order_10024 sent by CheckoutOrderPlacedSubscriber.php.
  4. OpenAI Ads Event Manager: Check the test event console to confirm incoming events reflect a 100% deduplication match rate.

Frequently Asked Questions

Where do I add the OpenAI Ads Pixel snippet in Shopware 6 storefront?

Add the pixel initialization snippet by extending storefront/layout/meta.html.twig inside your custom theme or plugin using Twig {% sw_extends %} and {% block layout_head_meta_tags %}.

How do I trigger server-side CAPI purchases in Shopware 6?

Create a custom Symfony EventSubscriber listening to CheckoutOrderPlacedEvent. In the onOrderPlaced handler, build the OpenAI CAPI payload with order amount, currency, SHA-256 hashed customer details, and dispatch a background HTTP request to https://bzr.openai.com/v1/events.

How are browser and server purchase events deduplicated in Shopware 6?

Both the browser thank-you page script (finish.html.twig) and the backend EventSubscriber send the exact same event_id formatted as sw6_order_{order.orderNumber}. OpenAI Ads deduplication logic matches this ID and attributes 100% of order value once.

How does Shopware 6 handle currency and amount formatting for OpenAI Ads CAPI?

OpenAI Ads requires amounts in minor units (e.g., 2999 for €29.99). Calculate minor units in PHP by multiplying order.price.totalPrice by 100 as an integer and providing the ISO 4217 currencyCode.