> ## Documentation Index
> Fetch the complete documentation index at: https://partner-integrations.voyado.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Technical details

This article covers the technical architecture of the Voyado Shopware integration. It is intended for developers implementing, extending, or debugging the integration.

## Architecture overview

The integration is a Shopware plugin that extends the storefront using Shopware's Twig template inheritance system. It injects a JavaScript module (`voyado.js`) and structured data attributes into specific storefront pages. The JavaScript reads these attributes at runtime and sends tracking events to the Voyado Accelerator API via `POST` requests with JSON bodies.

Stock-level sync runs as a server-side pipeline separate from browser tracking. A `product.written` webhook triggers the Accelerator API, which detects restock events and pushes stock levels to Engage on a scheduled interval.

No Shopware core files are modified. All template changes are non-destructive Twig block overrides.

## Script loading

Two scripts are loaded on every storefront page via the base template override:

* `voyado.js` — the Voyado integration script, served from the plugin bundle
* Redeal widget script — a companion script for the on-site messaging add-on

Both are loaded with the `defer` attribute and do not block page rendering.

## Data passing

Tracking data is passed from the Shopware PHP/Twig layer to the browser using hidden HTML elements with `data-*` attributes. The JavaScript reads these attributes at runtime to construct API call payloads. This keeps Twig templates clean and decouples data preparation from behavioural logic.

## Browser storage

| Storage type    | Key                | Contents                                       | Cleared when                                                |
| --------------- | ------------------ | ---------------------------------------------- | ----------------------------------------------------------- |
| Local storage   | `voyado-cid`       | Voyado contact ID                              | Not automatically — persists across sessions                |
| Local storage   | Cart ID            | Random identifier for the current cart journey | Not automatically                                           |
| Local storage   | Cart snapshot      | JSON of previous cart state for deduplication  | Overwritten on each cart tracking call                      |
| Session storage | Session ID         | Unique ID for the current browsing session     | Browser tab closed; or when visitor navigates to login page |
| Cookie          | `voyado-analytics` | Consent state (`1` = granted)                  | Consent revoked                                             |

Nothing is written to any storage before the `voyado-analytics` cookie is set.

## Cookie consent flow

The integration listens for two named browser events:

* `voyadoConsentGranted` — sets `voyado-analytics=1` and activates tracking
* `voyadoConsentRevoked` — removes the cookie and stops all tracking

The Shopware built-in cookie banner fires these events automatically. Third-party CMPs must be configured to dispatch them. See [Setting up](/docs/shopware/setting-up) for GTM and JavaScript examples.

## Contact identification

**Login-based:** When a logged-in customer visits their account page, the integration reads their Shopware customer ID and email from the page and calls `POST /api/v1/Shopware/store-contactid`. The returned Voyado contact ID is stored as `voyado-cid` in local storage.

**Soft identification:** Personalised email links include an `eclub` URL parameter containing an encrypted contact reference. On page load, the integration calls `POST /api/v2/shopware/getSoftDecryptionKey` to decrypt the parameter, then resolves the contact ID and stores it as `voyado-cid`.

## Template overrides

| Template file                       | What it injects or enables                                                                               |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `base.html.twig`                    | `voyado.js`, CSS, and `voyado-tracking-data` element (shop ID, sales channel ID, language) on every page |
| `product-tracking-script.twig`      | Server-side preparation of product and stock data — no browser output                                    |
| `buy-widget-form.html.twig`         | `voyado-product-tracking-buy-widget-form` data element; back-in-stock form when product is out of stock  |
| `cart-widget.html.twig`             | `voyado-cart-tracking` element with serialised cart line items (promotion lines excluded)                |
| `action.html.twig`                  | Product ID, product number, and minimum purchase quantity on listing page product cards                  |
| `index.html.twig` (checkout/finish) | Completed order line items as a JSON data attribute for cart conversion signal                           |
| `offcanvas-cart.html.twig`          | Placeholder element for the promotions widget in the mini-cart                                           |
| `login.html.twig`                   | Login page URL marker used to trigger session reset                                                      |

## Cart tracking

### Deduplication

Before each cart tracking call, the integration serialises the current cart line items and compares them to the snapshot stored in local storage. If the payload is identical, the call is suppressed. The snapshot is updated after each successful call.

### Triggers

| Trigger                           | Behaviour                                                                    |
| --------------------------------- | ---------------------------------------------------------------------------- |
| Add to cart button click          | 2-second delay, then reads cart contents from `voyado-cart-tracking` element |
| Cart page load (`/checkout/cart`) | Fires immediately on page ready                                              |
| Checkout confirm page load        | Fires immediately on page ready                                              |
| Checkout finish page load         | Fires with empty product array — signals cart conversion                     |

## Product tracking payload fields

The `voyado-product-tracking-buy-widget-form` element carries the following `data-*` attributes, which are included in the product tracking request:

* `data-product-id` — Shopware product ID
* `data-product-number` — product SKU
* `data-page-url` — URL of the product detail page
* `data-is-out-of-stock` — boolean stock status
* `data-language` — storefront language
* `data-sales-channel-id` — Shopware sales channel ID
* `data-shop-id` — Voyado shop identifier

The session ID and contact ID are added from browser storage at call time.

## Web tracking data flow

```text theme={null}
Visitor accepts consent
        │
        ▼
voyado-analytics=1 cookie set
        │
        ▼
Contact identification
  ├── Login → Store contact ID in Local storage → voyado-cid stored
  └── Email link (encrypted parameter) → Decrypt parameter → Store contact ID in Local storage → voyado-cid stored
        │
        ▼
Product page visit
  └── POST api/v3/productviews (contact ID, product ID, SKU, URL, session ID)
        │
        ▼
Add to cart
  └── POST /api/v3/carts (contact ID, cart line items, cart ID)
        │
        ▼
Cart page / checkout confirm
  └── POST /api/v3/carts (updated cart snapshot)
        │
        ▼
Order placed (checkout finish)
  └── POST /api/v3/carts (empty product array — cart closed)
```

***

## Back-in-stock: stock-level sync

Stock-level sync runs as a two-step server-side pipeline, independent of browser tracking.

### Pipeline overview

```mermaid theme={null}
sequenceDiagram
    participant SW as Shopware
    participant API as Accelerator API
    participant DB as App DB
    participant ENG as Engage

    SW->>API: POST product.written
    API->>API: Validate webhook signature
    API->>SW: Fetch live product + visibility data
    API->>DB: Store pending record (if restock)
    Note over API,DB: Scheduled job runs on interval
    DB->>API: Load pending records
    API->>ENG: PUT /api/v3/inventory/stock-levels
    ENG-->>API: 200 OK
    API->>DB: Mark records as Success
```

### Step 1 — Webhook ingestion

When a product record is written in Shopware, Shopware fires a `product.written` event and sends a webhook to the Accelerator API. The API validates the request using the `shopware-shop-signature` header — requests that fail validation are rejected before any processing.

After validation, the API fetches live product data and sales channel visibilities directly from Shopware using the product ID from the payload.

A pending stock update record is only created when both conditions are met:

* The product's previously stored quantity was ≤ 0
* The product's current `availableStock` from Shopware is > 0

Quantity changes that do not cross the zero boundary are ignored.

### Webhook registration

All webhooks are registered automatically when the Voyado app is installed in Shopware. The inventory webhook is registered for the `product.written` event.

### Sales channel and site matching

Each product visibility from Shopware includes a `salesChannelId`. The integration matches this against your configured site and channel mapping.

<Warning>
  **Back in stock** must be enabled in the Accelerator configuration for each sales channel you want stock-level updates to flow through. Updates for channels where this is not enabled are silently dropped.
</Warning>

### Step 2 — Scheduled push to Engage

A scheduled job polls for pending records and sends them to Engage using `PUT /api/v3/inventory/stock-levels`.

Payload fields per record:

<ResponseField name="Quantity" type="integer" required>
  The current available stock quantity for the product.
</ResponseField>

<ResponseField name="Sku" type="string" required>
  The product number in Shopware.
</ResponseField>

<ResponseField name="ExternalId" type="string" required>
  The Sales Channel ID in Shopware.
</ResponseField>

Records remain in `Pending` state until Engage returns a successful response. They are picked up on subsequent scheduled runs until delivery succeeds.

***

## Back-in-stock: subscription flow

The subscription flow is independent of the stock-level sync pipeline. It records a customer's opt-in to be notified when an out-of-stock product restocks, and writes that subscription to Engage.

### Storefront payload

On form submission, the storefront posts to the Accelerator API:

```json theme={null}
{
  "shop_id": "<shopware-shop-id>",
  "saleschannel_id": "<shopware-sales-channel-id>",
  "shopUrl": "<shopware-domain>",
  "email": "customer@example.com",
  "product_id": "<shopware-product-uuid>",
  "language_id": "<shopware-language-uuid>",
  "accept_email": "on"
}
```

On success, the API returns: `"Thank you! We will notify you when the product is available."`

### Processing pipeline

<Steps>
  <Step title="Configuration and gate checks">
    The API resolves the site configuration using `shop_id` and `saleschannel_id`. The call returns early with no subscription created if either of the following is not enabled:

    * Shopware integration enabled
    * Back in stock feature enabled
  </Step>

  <Step title="Product and locale lookup">
    The API makes two calls to Shopware:

    * `GET /api/product/{product_id}` — the `productNumber` field becomes the `Sku` on the subscription
    * `GET /api/language/{language_id}/locale` — the locale code (for example, `en-GB`) becomes the `Locale` field
  </Step>

  <Step title="Contact resolution in Engage">
    The API resolves or creates a contact in Engage based on the submitted email address and marketing consent value:

    | Scenario                                          | Outcome                                                                                         |
    | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
    | Contact exists + `acceptsEmail` = `true`          | Existing contact updated: `acceptsEmail` set to `true`                                          |
    | Contact does not exist + `acceptsEmail` = `true`  | New contact created with `acceptsEmail` = `true`                                                |
    | Contact does not exist + `acceptsEmail` = `false` | New contact created with `acceptsEmail` = `false`                                               |
    | Contact exists + `acceptsEmail` = `false`         | Existing contact unchanged — `acceptsEmail` preference is not overridden from `true` to `false` |
  </Step>

  <Step title="Subscription creation in Engage">
    Once a valid contact ID is resolved and the locale lookup succeeds, the API posts the subscription to Engage:

    ```http theme={null}
    POST /api/v3/inventory/backinstock/subscriptions
    apikey: <your Engage API key>
    Content-Type: application/json

    {
      "contactId": "<engage-contact-id>",
      "sku": "<shopware-productNumber>",
      "locale": "en-GB",
      "externalId": "<shopware-product-uuid>"
    }
    ```

    Engage returns `201 Created` for a new subscription or `409 Conflict` if the contact already has an active subscription for that SKU. Both responses are treated as success — `409` is the expected response on duplicate submissions.
  </Step>
</Steps>

### Subscription payload fields

<ResponseField name="ContactId" type="string" required>
  The Engage contact ID resolved from the customer's email address.
</ResponseField>

<ResponseField name="Sku" type="string" required>
  The product number from Shopware (`productNumber`).
</ResponseField>

<ResponseField name="Locale" type="string" required>
  The locale code resolved from the Shopware language record, for example `en-GB`.
</ResponseField>

<ResponseField name="ExternalId" type="string" required>
  The Shopware product UUID.
</ResponseField>

***

## Technical considerations

* The `voyado-cid` in local storage persists across browser sessions with no automatic expiry. If a different contact logs in on the same browser, `voyado-cid` is overwritten only after the new contact's ID is successfully resolved. The session ID is reset when the visitor navigates to the login page.
* The 2-second delay on add-to-cart tracking is hardcoded to allow Shopware's cart update to complete before the cart contents are read. This delay is not configurable.
* Listing page add-to-cart events use the product data from `action.html.twig`, which does not include the product detail page URL — only the product ID, product number, and minimum purchase quantity.
* Stock-level sync only activates for restock events (quantity crossing from ≤ 0 to > 0). Quantity changes that do not cross the zero threshold are not forwarded to Engage.
