---
title: Inserting Entity Data via Hooks | Shopware Community Hub
description: Here we will leverage the power of ‘hooks’ in the App System.
canonical_url: 'https://hub.shopware.com/learn/unit/entity-data-via-hooks'
---

# Inserting Entity Data via Hooks

<LearningObjectives>

- **Understand** what **Webhooks** are and their role in the Shopware App System.
- **Define a webhook in the `manifest.xml`** and connect it to the App Server.
- **Implement** a simple **Controller** that **reacts to a webhook event** and performs an operation.
- **Learn** what **App Lifecycle events** are and how they can be used.

</LearningObjectives>

# Inserting Entity Data via Hooks

In this learning unit, you will learn how to use **Webhooks** in your App Server. Webhooks allow your app to react to events in Shopware, such as an app activation or customer login.

We will configure a Webhook in the `manifest.xml` and implement a Symfony Controller to handle it. Our example will automatically insert _Physical Shop_ data when the app is activated.

## App-Script Hooks and Webhooks

### Reminder: App Script Hooks

In the previous course, you already worked with [**hooks**](https://developer.shopware.com/docs/guides/plugins/apps/app-scripts/#script-hooks) in the context of [App Scripts](/learn/unit/app-sourcing-data-via-app-scripts).

Hooks are **Shopware events** (e.g., `product-page-loaded` or `landing-page-loaded`) that you can react to with Twig scripts. With it, you can **attach or modify data** that templates later render. All of this happens **inside Shopware**.

To check your memory, here is a quick question:

<ArticleMultipleQuestionnaire>
  <ArticleQuestionnaire>
    <ArticleQuestionnaireQuestion>What can App Scripts via hooks do? (Multiple answers possible.)</ArticleQuestionnaireQuestion>
    <ArticleQuestionnaireAnswer correct>Attach custom data to storefront pages.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>Automate tasks in the Shopware Admin.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer correct>React to storefront hooks and modify page data.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>Change global Shopware configuration.</ArticleQuestionnaireAnswer>
  </ArticleQuestionnaire>
</ArticleMultipleQuestionnaire>

### Webhooks

Now let's extend this idea to [**webhooks**](https://developer.shopware.com/docs/guides/plugins/apps/webhook.html).

Webhooks also subscribe to **Shopware events** (e.g., `app.activated`, `order.placed`). But instead of running Twig scripts inside Shopware, you define webhooks in your App's `manifest.xml` and specify three things: a **webhook name**, the **URL of your external App Server** and the **Shopware event** to subscribe to.

When the **event is triggered**, Shopware sends a **POST request** to the corresponding URL. This request contains:

- The **Event type** (e.g., `order.placed`).
- The **Payload data** (e.g., order details, customer info, ...).
- **Security headers** for verification.

This way, your App Server can react to Shopware events, for example, syncing an order to an ERP system.

<Callout title="Lifecycle Cleanup (Best Practice)" type="info">

Lifecycle events (e.g., `app.activated`, `app.deleted`) notify your App Server through registered webhooks. Your App Server should handle these events carefully:

- **On activation/install**: Initialize resources, seed default data, establish credentials.
- **On deactivation/uninstall*: Revoke tokens/credentials, delete app-specific data, and clean up state.

This ensures a healthy App lifecycle and prevents orphaned credentials or stale data.

</Callout>

### **Webhooks** vs **App Scripts Hooks**

- **Hooks (App Scripts)**: Subscribe to Shopware events (e.g., `product-page-loaded`) and run Twig code. Everything happens **inside Shopware**.
- **Webhooks (App Server)**: Subscribe to the Shopware events (e.g., `order.placed`), but Shopware notifies your **App Server via POST request**, so your external service can run logic **outside Shopware** like the example of syncing with an ERP System above.

In the following, you will get a better picture about webhooks.

## Code-Along

To follow along, use the following command within the directory containing your `ShopwareStoreTrackerBackend` repository:

```bash
git checkout tags/entity_data_via_hooks--start
```

## A Bit of Context

The term **webhook** gives us a hint about its purpose: it **hooks** into events and processes that happen during the customer journey in a Shopware shop. Every time a customer takes an action (e.g., loading the Startpage, logging in, opening the product detail page, ...), Shopware triggers **events**. These events can then be:

- **Handled internally** with App Script Hooks (Twig).
- **Sent externally** to your external App Server via Webhooks (HTTP POST requests).

To make it more concrete: imagine the customer browsing a Shopware shop, first the start page, then he is logging in, then he opens the product detail page. At each step, Shopware triggers one or more events (hooks), which you can subscribe to.

![Illustration of browsing journey in context of Webhooks](assets/app-hook-journey.png)

_The blue nodes are steps in the consumer browsing journey, while the green nodes are hooks fired in response to the respective stage._

### Check Which Hooks are Firing With Symfony Profiler

If you want to check which hooks are firing on a specific page, you can use the [Symfony Profiler](https://symfony.com/doc/current/profiler.html). Click the green `200` status code at the bottom of the page to open the Profiler.

![Demonstration of Profiler link](assets/200-button.png)

Go to the **Scripts** tab in the sidebar to see which App Scripts were executed. The tabs at the top of the Profiler page will show the hooks/events that fired (in this case: `cart`, `product pricing`, `navigation-page-loaded`, `response`).

![Symfony Profiler Script tab](assets/symfony-profiler-scripts-page.png)

## What We Need to Do

In a previous course, you uploaded `physical_shop` entity records manually via [Postman](https://www.postman.com/). That was fine for demonstration purposes of how the Shopware Admin API works, but in a real-world scenario, this approach is:

- Impractical: You don't want to upload entities by hand every time.
- Risky: Exposing credentials in an API client like Postman can be a security issue.

Now we will improve this process using **Webhooks**.

- When the app gets activated with the console command `bin/console app:activate [app_name]`, Shopware will trigger the `app.activated` event.
- We subscribe to this event with our own webhook in the `manifest.xml` file.
- The webhook notifies our App Server, which will then call the Shopware Admin API and automatically create `physical_shop` entities with predefined data.

This way, the setup is **automated, secure and reproducible**.

## Webhook Configuration in Apps

By default, when Shopware fires an event, only **internal subscribers** (like App Scripts or core services) are notified. If you want your App Server to also receive the event, you must explicitly **register a webhook** in your App's `manifest.xml`.

### Defining a New Webhook

Our `ShopwareStoreTracker` app already defines [lifecycle webhooks](https://developer.shopware.com/docs/guides/plugins/apps/app-sdks/php/02-lifecycle.html) in the `manifest.xml`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd">
  <meta>
  ...
  </meta>
  <webhooks>
    <!-- existing app.activated webhook -->
    <webhook name="appActivated" url="http://localhost:8001/app/lifecycle/activate" event="app.activated"/>
    <webhook name="appDeactivated" url="http://localhost:8001/app/lifecycle/deactivate" event="app.deactivated"/>
    <webhook name="appDeleted" url="http://localhost:8001/app/lifecycle/delete" event="app.deleted"/>
  </webhooks>
</manifest>
```

These webhooks are added by the Shopware PHP SDK and handle basic setup for the app. Now, we want to add our **own** webhook for the same event (`app.activated`). To avoid conflicts, we give it a unique name and point it to a new URL in our App Server:

```xml
<!-- sstAppActivated => "ShopwareStoreTracker App Activated" -->
<webhook name="sstAppActivated" url="http://localhost:8001/upload/physical-stores" event="app.activated"/>
```

Increment the minor version number (e.g., `1.0.3` -> `1.0.4`) within your `manifest.xml` file and run the following command:

```bash
bin/console app:update
```

Shopware will insert a new row into the database's `webhook` table.

![Output of SELECT query on webhook table after running App Update](assets/webhook-created-inspection.png)

_New webhook record named `sstAppActivated` (with event target `app.activated`) appearing within Adminer search results._

Now that the custom webhook has an entry in the database, Shopware knows:

1. The event to listen to: `app.activated`.
2. Who to notify: our App Server at `http://localhost:8001/upload/physical-stores`.

### Configuring an Endpoint

On the App Server side, we need to implement a [Symfony Controller](https://symfony.com/doc/current/controller.html) that receives the POST request from Shopware. The controller will get [`WebhookAction`](https://github.com/shopware/app-php-sdk/blob/main/src/Context/Webhook/WebhookAction.php) object injected, which gives access to:

- The **Shop** that triggered the event.
- The **Event name** (e.g., `app.activated`).
- (Optional) The **payload data**, depending on the event.

According to the [webhook event reference](https://developer.shopware.com/docs/resources/references/app-reference/webhook-events-reference.html), when we react to the `app.activated` event we will not receive a payload of data via the `WebhookAction` argument. The intention here is to simply batch upload generated entity data on the App Server, so this is not an issue, however, some applications of data payloads may include:

| Event                     | Payload           | Example use case                                                                         |
|---------------------------|-------------------|------------------------------------------------------------------------------------------|
| `order.placed`            | `Order` object    | Send order details to an ERP system to update stock or trigger fulfillment.              |
| `checkout.customer.login` | `Customer` object | Track login activity in your analysis system (e.g., country of origin, login frequency)  |

## The Symfony Controller

The Symfony controller executes business logic when called upon by a webhook. The method executed may rely upon various [Service Arguments](https://symfony.com/doc/current/service_container.html#service-parameters) to complete its designated operations.

Typically, classes within a Symfony Application require a [corresponding service definition](https://symfony.com/doc/current/controller/service.html) to be provided any service dependencies. However, current versions of the Symfony Framework allow the `AsController` annotation which takes care of supplying Service Dependencies automatically.

<Callout title="App-Server Logic is Platform-independent" type="info">

As mentioned previously, this course uses the [Symfony PHP SDK](https://github.com/shopware/app-bundle-symfony) to demonstrate the development of the App Server.

Keep in mind that the principle of webhooks is **technology-independent**. Once you understand how [request signing works](https://developer.shopware.com/docs/guides/plugins/apps/app-signature-verification.html) in the App system, you can apply the same logic in any language (e.g., JavaScript, Python, or Go).

Shopware always sends an HTTP POST request to the webhook URL with the event and payload. All your App Server needs to do is expose an HTTP endpoint that can receive and process this request.

</Callout>

### Our Case: The UploadController

In Symfony, a controller can declare any **public methods a route** by using [the `Route` annotation](https://symfony.com/doc/current/routing.html#creating-routes-as-attributes).

In our case, the route path is `/upload/physical-stores` and it listens for the HTTP method **POST**.

Below you can see the complete `UploadController` class:

```php
<?php declare(strict_types=1);

namespace App\Controller;

use Shopware\App\SDK\Context\Webhook\WebhookAction;
use Shopware\App\SDK\HttpClient\ClientFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Faker\Factory;
use Faker\Generator;

#[AsController]
class UploadController
{
    // Relative API path for sending data back to the Shopware API
    private const SHOPWARE_API_PATH = "/api/ce-physical-shop/";

    // Maximum number for demo records to upload
    private const UPLOAD_LIMIT = 10;

    private ClientFactory $clientFactory;
    private Generator     $faker;

    public function __construct(ClientFactory $clientFactory)
    {
        $this->clientFactory = $clientFactory;
        $this->faker         = Factory::create();
    }

    #[Route('/upload/physical-stores', methods: ['POST'])]
    public function handle(WebhookAction $webhook): Response
    {
        $client     = $this->clientFactory->createSimpleClient($webhook->shop);
        $requestUrl = $webhook->shop->getShopUrl() . self::SHOPWARE_API_PATH;

        for ($i = 0; $i < self::UPLOAD_LIMIT; $i++) {
            $shopData = [
                "name" => $this->faker->company(),
                "streetAddress" => [
                    "street" => $this->faker->streetName(),
                    "townOrCity" => $this->faker->city(),
                    "countyOrProvince" => null,
                    "zipCode" => $this->faker->postcode(),
                ],
                "country" => $this->faker->country(),
                "description" => $this->faker->paragraph(),
                "email" => $this->faker->email(),
                "phone" => $this->faker->phoneNumber(),
            ];

            $client->post(
                $requestUrl,
                $shopData,
            );
        }
        // Return HTTP 204 (No Content) - Webhook handled successfully
        return new Response(null, 204);
    }
}
```

**Code Explanation:**

- The `#[AsController]` attribute allows Symfony to automatically inject the `WebhookAction` argument into the `handle` method.
- The route `/upload/physical-stores` matches the URL defined in your webhook.
- `SHOPWARE_API_PATH` is the **custom entity endpoint** – that we defined in the `manifest.xml` file – for sending data back to Shopware.
- `UPLOADED_LIMIT` defines how many entities are uploaded.
- The **[Faker](https://fakerphp.org/)** library generated random company and address data for demonstration purposes.
- `ClientFactory` provides an authenticated HTTP client for the correct Shop. With a [SimpleHttpClient](https://developer.shopware.com/docs/guides/plugins/apps/app-sdks/php/05-http-client.html) from the PHP SDK, we call the Shopware Admin API and create new records.
- Returning `204 No Content` signals to Shopware that the webhook was processed successfully – the webhook is **acknowledged**.

<Callout title="Shortcut with Maker Bundle" type="info">

You can use the [Symfony Maker Bundle](https://symfony.com/doc/current/the-fast-track/en/6-controller.html#generating-a-controller) to quickly generate a controller skeleton via CLI. Note that you still need to add the `#[AsController]` attribute manually.

</Callout>

#### Request Authentication by the PHP SDK

All webhook requests sent from Shopware are **digitally signed** using HMAC.

The **Shopware PHP SDK** automatically verifies this signature **before** executing your controller logic.

This ensures that only **registered and authenticated Shops** can access your endpoints, and that no external or tampered requests can reach your App Server.

For more details, see the official docs: [App Signature Verification](https://developer.shopware.com/docs/guides/plugins/apps/app-signature-verification.html).

#### Handling Webhooks via Symfony SDK Events

Instead of handling webhooks directly, the [Shopware Symfony SDK bundle](https://github.com/shopware/app-bundle-symfony/tree/main) lets you treat webhook calls as native [Symfony Events](https://symfony.com/doc/current/event_dispatcher.html).

You can then respond to activations via listeners or subscribers, just like any other Symfony event.

This approach can simplify your codebase, but in this course we stick to a basic webhook controller implementation. Know that the concept is also cross-compatible with the JavaScript SDK.

## Code-Along (end)

To see what the final result of this learning unit should look like, run the following command:

```bash
git checkout tags/entity_data_via_hooks--end
```

<Callout title="Removing Working Changes" type="warning">

If you have local changes, you will need to run `git reset --hard HEAD` in the App directory. Be mindful that this command will destroy any local changes!

</Callout>

## Summary

In this learning unit, you learned:

- What **webhooks** are and how the cycle between Shopware and App Server works
- How to **define** a new webhook in your App's `manifest.xml`
- How to **implement** a Symfony Controller, in this case in our localhost App Server, that receives a webhook request

With this, you have created your first webhook flow: Shopware event → Webhook → App Server endpoint → API call back to Shopware.
