Shopware 6:OpenAI Ads ピクセル&コンバージョンAPI(CAPI)完全導入設定ガイド
Shopware 6ストア向けOpenAI Adsトラッキング実装手順。Twigテンプレート拡張とSymfony EventSubscriberによる堅牢なサーバーサイドCAPI連携。
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
- Pixel Loading: Inspect the browser console network tab for requests to
https://bzrcdn.openai.com/pixel.jsreturning HTTP 200. - Event Payload: Verify that
Purchaseevents containvaluein cents (e.g.12900for €129.00) and proper uppercase currency codes (EUR,USD). - Deduplication Matching: Ensure the
event_idsent infinish.html.twigmatchessw6_order_10024sent byCheckoutOrderPlacedSubscriber.php. - 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.