shopware/app-php-sdk
A PHP SDK for building Shopware apps. It handles app registration and lifecycle, webhook verification, HMAC request signing, and authenticated communication with the Shopware API, giving PHP developers ready-made tools for secure, reliable app development on the Shopware platform.
The SDK is deliberately thin: it handles Shopware app protocol mechanics (signatures, registration proof, lifecycle payloads, API client plumbing, context hydration) and leaves HTTP framework, persistence, and routing to you. Expect to write more glue code than with a framework bundle.
composer require shopware/app-php-sdk \
nyholm/psr7 \
guzzlehttp/guzzle
ShopRepositoryInterface. You cannot use the SDK without implementing persistence for shops. A shop is identified by its Shopware shop ID and stores at minimum the shop URL, shop secret, and API credentials. Start with a simple Doctrine/Eloquent/Redis-backed implementation.use Shopware\App\SDK\Shop\ShopInterface;
use Shopware\App\SDK\Shop\ShopRepositoryInterface;
final class ShopRepository implements ShopRepositoryInterface
{
public function __construct(private \PDO $pdo) {}
public function createShop(ShopInterface $shop): void { /* INSERT ... ON CONFLICT UPDATE */ }
public function updateShop(ShopInterface $shop): void { /* ... */ }
public function deleteShop(string $shopId): void { /* ... */ }
public function getShop(string $shopId): ?ShopInterface { /* ... */ }
}
AppLifecycle with your repository and app secret, then expose the lifecycle endpoints. Your first useful endpoint is registration (POST /registration): it validates the shopware-app-signature query parameter, gives you a RegistrationRequest containing shop metadata, and produces the proof/secret response Shopware expects.$lifecycle = new AppLifecycle($shopRepository, $_ENV['SHOPWARE_APP_SECRET']);
$registration = $lifecycle->register($request); // validates app signature
$shopRepository->createShop($registration->getShop());
return $registration->getResponse(); // JSON with proof/secret
activate, deactivate, uninstall, and the webhook endpoint. Registration plus one webhook is the smallest end-to-end slice that proves signatures, persistence, and the API client all work.Single verifier in middleware. Put signature verification in one PSR-15 middleware (or Laravel middleware), resolve the shop once, and attach it to the request as an attribute. Every downstream handler then receives a trusted ShopInterface instead of re-parsing headers. This mirrors how the SDK's own context resolution is intended to be used and avoids duplicated HMAC logic across controllers.
Registration as upsert. Registration can be retried and can re-register an existing shop (for example after a secret rotation). Treat shop-id as the primary key and upsert; never assume a shop is new. Persist the shopware-version too — it is your only reliable feature gate for API differences.
Webhook handling: verify, resolve, enqueue, return 200. Shopware retries failed webhooks aggressively. Do the minimum synchronously:
$webhook = $lifecycle->webhook($request); // verifies shopware-shop-signature
$shop = $webhook->getShop();
$queue->push(new HandleShopwareEvent($shop->getShopId(), $webhook->getPayload()));
return new Response(200);
Make the job idempotent. Shopware may deliver the same event more than once, and the payload does not carry a globally unique delivery ID you can rely on across versions, so derive idempotency from event name + entity ID + updatedAt, or store the raw payload hash.
Use ShopwareClient for both APIs. Build the client from the persisted shop; it carries the credentials and gives you PSR-7 request builders. Use Admin API credentials (client_id/client_secret) for /api/... and Store API credentials (sw-access-key) for /store-api/.... Do not hand-roll OAuth token fetching unless you need to share a token cache across processes.
$client = ShopwareClient::createFromShop($shop, $httpClient, $requestFactory);
$response = $client->send($client->createGetRequest('/api/product/' . $id));
Typed contexts instead of arrays. For payment and checkout flows, let ContextResolver turn the raw request into typed action objects (PaymentValidateAction, PaymentPayAction, PaymentCaptureAction, etc.). This is the SDK's biggest day-to-day win: you avoid $payload['order']['lineItems'][0]['...'] spelunking and get IDE support. Payment handlers become small match-style dispatch methods.
Framework integration. For Laravel/Symfony, register AppLifecycle, the repository, the PSR-18 client, and the request factory as singletons. Keep the SDK objects stateless; the only per-request state should be the resolved shop. Route registration/activation/webhook endpoints outside CSRF and auth middleware, but inside your normal logging and error-handling middleware.
Testing. Because the SDK is framework-agnostic, you can test handlers without booting an app: construct signed PSR-7 requests with a known app secret or shop secret, call AppLifecycle, and assert on the returned response and repository calls. Keep a small fixture factory for signed registration and webhook requests — you will use it constantly.
hash_equals verification. Read php://input or the PSR-7 body once and verify before any middleware mutates it.shopware-app-signature query param). Webhooks and lifecycle calls use the shop secret (shopware-shop-signature header). Mixing them up is the single most common integration bug. Log which verifier failed, not just "invalid signature".null, respond 200 (or 410) and log; throwing gives Shopware a retry loop you cannot satisfy.shop-url values can include or omit trailing slashes and may be http in local dev. Normalize once at persistence time and build API URLs from a single helper; never concatenate shopUrl . '/api' inline.ClientInterface::sendRequest throws on transport errors and sometimes on 4xx/5xx depending on configuration. Wrap calls and translate to your domain exceptions; do not let a Guzzle exception escape a webhook handler.shopware-shop-signature vs shopware-app-signature), check that a proxy is not rewriting the body, and always compare with hash_equals.composer.json, read the source for the classes you use, and treat upgrades as minor migrations rather than drop-ins. The source is readable in an afternoon — that is the intended debugging strategy.ShopInterface implementation for extra persisted fields (tenant ID, locale, flags), a decorator around your repository for caching, a decorator around the PSR-18 client for logging/retries, and custom context resolvers for your own webhook payload types. Avoid subclassing AppLifecycle; compose it in a small application service instead, which keeps upgrades tractable.How can I help you explore Laravel packages today?