Shopware 6 offers two different ways of reacting to something happening in a shop, and they are easy to confuse. One is the internal PHP event system, which plugins hook into and which runs inside the Shopware process. The other is webhooks, which are part of the App System and notify an external service over HTTP.
Choosing the wrong one costs time. A plugin subscriber cannot run on a Shopware Cloud instance, and a webhook cannot change what happens during a request. This guide explains what each mechanism does, how a webhook is registered and secured, and which behaviours are documented versus which you should not rely on.
All technical details below refer to the official Shopware developer documentation. Version-dependent features are marked with the release they were introduced in.
Shopware plugins run inside the shop. They register a subscriber that implements EventSubscriberInterface, tag it with kernel.event_subscriber, and receive events synchronously while the request is still being processed. Because the subscriber holds the event object, it can influence what happens next: adjust a price, block a write, change what the storefront renders.
Apps run outside the shop. Shopware communicates with them exclusively over HTTP, and webhooks are how the shop tells the app that something happened. The app receives a POST request after the fact and reacts by calling back into the Admin API. It cannot alter the process that triggered it.
A useful rule of thumb: if the logic has to run during the request and change its outcome, it belongs in a plugin. If it reacts after the fact and lives on your own infrastructure, it belongs in an app with webhooks.
Before the first webhook fires, four things need to be in place.
Webhooks belong to an app, not to a plugin. The app is defined by a manifest.xml and completes a registration handshake with the shop, during which your app generates and stores a per-shop secret. That secret is what later signs every webhook request.
Webhook delivery is queued through Shopware’s message queue. If no consumer is running, messages are written but never sent, and no error appears in the storefront. This is the single most common reason a correctly configured webhook appears to do nothing.
Shopware posts to the URL declared in the manifest. It must be reachable from the shop server and should terminate TLS. For local development, a tunnelling service is required so that the shop can reach your machine.
Most webhook events require a corresponding permission in the manifest. Subscribing to order.written without declaring order:read will fail validation. Shopware has enforced webhook permission validation since 6.3.5.0.
There is no webhook plugin to install. Webhooks are declared directly in the app manifest, inside a <webhooks> block:
<webhooks>
<webhook name="product-changed"
url="https://example.com/event/product-changed"
event="product.written"/>
</webhooks>
Three attributes are required. name must be unique within the manifest, url is the endpoint Shopware posts to, and event is the identifier of the event you want to receive.
Only events that Shopware marks as hookable can be used. The documented set covers the areas most integrations need:
checkout.order.placed, order.written, order.deletedproduct.written, product.deleted, customer.written, category.written, media.written and their counterpartscheckout.customer.register, checkout.customer.login, checkout.customer.logout, customer.recovery.requeststate_enter.order.state.completed, state_enter.order_transaction.state.paid, state_enter.order_delivery.state.shipped and the matching state_leave eventsapp.installed, app.activated, app.deactivated, app.deleted, plus shopware.updatedSince Shopware 6.5.7.0, a webhook can carry onlyLiveVersion="true". Without it, an order.written webhook also fires for versioned drafts created while an order is being edited in the administration, which usually is not what an integration wants:
<webhook name="order-created"
url="https://example.com/event/order-created"
event="order.written"
onlyLiveVersion="true"/>
The attribute is only evaluated for entity-written events.
The request body is JSON and always has the same three top-level keys:
{
"data": {
"payload": [
{
"entity": "product",
"operation": "delete",
"primaryKey": "7b04ebe416db4ebc93de4d791325e1d9",
"updatedFields": []
}
],
"event": "product.written"
},
"source": {
"url": "http://localhost:8000",
"appVersion": "0.0.1",
"shopId": "dgrH7nLU6tlE",
"eventId": "7b04ebe416db4ebc93de4d791325e1d9"
},
"timestamp": 123123123
}
For entity-written events the payload does not contain the full entity. Shopware states the reason plainly: because delivery is asynchronous, a complete entity might already be outdated by the time it arrives. What you get is the primaryKey, and the expected pattern is to fetch the current state through the Admin API.
Integrations that treat the payload as the source of truth will eventually write stale data back into a connected system. This is worth deciding before the first line of code, not after.
source.shopId identifies the shop and stays constant for its lifetime.source.eventId stays the same across retries, which makes it the natural key for deduplication.timestamp is the time the webhook was handled and is available from 6.4.1.0 onwards.Shopware also sends a sw-version header from 6.4.1.0, and sw-context-language plus sw-user-language from 6.4.5.0.
Every webhook request carries a shopware-shop-signature header. It contains a SHA-256 HMAC of the raw request body, keyed with the shop secret your app generated during registration. An endpoint that skips this check accepts anything anyone posts to it.
The App System uses two different secrets, and mixing them up is a common source of failed verification:
shopware-app-signature signs the query string of the registration request and uses the app secret shared with your Shopware Account.shopware-shop-signature signs the webhook request body and uses the shop secret created per shop during registration.Hash the raw body, before any JSON parsing. Frameworks that re-serialise the request will produce a different byte sequence and therefore a different HMAC.
Reject requests whose timestamp is too old. An attacker cannot alter the timestamp without invalidating the signature, which makes it a usable defence against replayed requests.
Shopware recommends using its App PHP SDK or Symfony Bundle rather than implementing verification by hand. Since 6.7.13.0, the signing secret is resolved at delivery time, so deliveries that span a secret rotation are signed with the current secret.
Webhooks fail quietly. Nothing in the storefront indicates that a delivery did not happen, so the first step is always to establish whether Shopware tried at all.
Shopware records delivery attempts in webhook_event_log. If there is no row for the event you expected, the webhook was never queued and the problem is in the manifest or the permissions. If there is a row that never leaves the queued state, the message queue is the problem, not your endpoint.
A cleanup task removes entries that were successfully delivered or permanently failed, while entries still running or being retried are preserved. Since 6.7.4.0, entries stuck in the queued state are removed after double the configured retention period.
onlyLiveVersion is not set.A minimal endpoint that logs the raw body and returns 200 quickly separates delivery problems from processing problems. Once deliveries arrive reliably, move the processing behind a queue of your own.
The design decisions that keep a webhook integration stable are made early, and most of them follow from one property: delivery is asynchronous and may be retried.
shopware-shop-signature against the raw body before anything else happens.source.eventId, which stays the same across retries, to recognise a request you have already processed.primaryKey through the Admin API.Two behaviours are deliberately absent from this list because Shopware does not document them: the exact HTTP timeout for a delivery, and which status codes are treated as failure. Any integration that depends on specific values there is building on an assumption rather than a specification.
No. Webhooks are part of the App System and are declared in the app manifest. There is nothing to install from the Shopware Store.
In most cases no message queue consumer is running. Shopware queues the delivery and never sends it. Check webhook_event_log to see whether the event was queued at all.
Because delivery is asynchronous and a full entity could already be outdated on arrival. Shopware sends the primaryKey and expects you to fetch the current state through the Admin API.
The webhook documentation does not state a webhook-specific number. Shopware’s general message queue documentation says messages are retried three times before being discarded to the failure transport. Treat that as the expected order of magnitude, not as a guarantee, and use source.eventId for deduplication.
No. A webhook is a notification after the fact. To influence a running request you need a plugin with a PHP event subscriber, which in turn means the shop must be self-hosted.
App System webhooks have been part of Shopware core since the 6.3 line. Several details arrived later: the timestamp field and sw-version header in 6.4.1.0, the language headers in 6.4.5.0, and onlyLiveVersion in 6.5.7.0. The current major line is 6.7.