1 minute to read

Introducing the Shopware SDK for Go: A foundation for building Shopware Apps

Introducing the Shopware SDK for Go: A foundation for building Shopware Apps

On 9 November we published the Shopware SDK for Go. It gives Go developers the same foundation that the PHP and JavaScript SDKs give their ecosystems: app registration, signed webhooks and requests, shop storage and an authenticated Admin API client. The SDK is open source under the MIT license, supports Go 1.26 and 1.27, and ships as three independently versioned modules, all at v1.0.0.

This post walks through building a small Shopware app with it: registering the app, reacting to a product webhook, adding an action button to the Administration and calling the Admin API. At the end we look at the part that is new compared to the other SDKs: an Admin API client you can use on its own, without the app system.

What does the SDK offer?

  • Registration of the app with a shop, including the confirmation step, secret rotation and the activate, deactivate and delete lifecycle events.

  • Verification and signing: HMAC-SHA256 checks on every webhook and signed request, replay protection with a freshness window, and signed responses back to Shopware.

  • Shop storage behind one interface, with in-memory, SQLite and DynamoDB implementations.

  • Admin API client with OAuth token management, one shared token per shop and pluggable token storage (Redis, Valkey or any key-value store).

  • Response builders for notifications, reloads, modals, payments and refunds.

  • Gin integration that registers all standard app routes with one call. The core works with net/http or any other router.

Shopware SDK for Go – Architecture

When to use it

  • Your app backend is a Go service, or you want a small static binary that runs anywhere.

  • You run several instances and want shop credentials and Admin API tokens in shared storage (SQLite or DynamoDB for shops, Redis or Valkey for tokens) without writing that plumbing yourself.

  • You only need the Admin API, not the app system: a CLI tool, a worker or an integration that talks to a shop with integration credentials. Install just clients/adminapi.

How to get started?

We use Go 1.26 or newer and the Gin web framework. The core SDK has no framework dependency; Gin support lives in its own module, so you only pull it in when you want it.

  1. mkdir go-shopware-app && cd go-shopware-app
  2. go mod init example.com/go-shopware-app
  3. go get github.com/shopware/sdk-go
  4. go get github.com/shopware/sdk-go/integrations/gin

Create a main.go that configures the app server. The SQLite store keeps registered shops between restarts and needs no CGo, so the binary builds with CGO_ENABLED=0.

  1. package main
  2. import (
  3. "log"
  4. "log/slog"
  5. "github.com/gin-gonic/gin"
  6. ginintegration "github.com/shopware/sdk-go/integrations/gin"
  7. "github.com/shopware/sdk-go/store"
  8. )
  9. func main() {
  10. shopStore, err := store.NewSQLiteStore(store.SQLiteConfig{DatabasePath: "./shops.db"})
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. defer shopStore.Close()
  15. router := gin.Default()
  16. logger := slog.Default()
  17. err = ginintegration.Configure(router, ginintegration.Config{
  18. AppName: "GoShopwareApp",
  19. AppSecret: "my-app-secret-min-8-chars",
  20. ConfirmationURL: "http://localhost:3000/app/register/confirm",
  21. ShopStore: shopStore,
  22. Logger: logger,
  23. })
  24. if err != nil {
  25. log.Fatal(err)
  26. }
  27. log.Fatal(router.Run(":3000"))
  28. }

Configure registers GET /app/register, POST /app/register/confirm and the three lifecycle routes POST /app/activate, /app/deactivate and /app/delete. Every one of them verifies the Shopware signature and signs its response. Start the server:

  1. go run .

Registering your app with Shopware

Create the app in your Shopware installation under custom/apps/GoShopwareApp with a manifest.xml. The registration URL and the webhooks point at the routes the SDK registered. The secret must match AppSecret.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd">
  4. <meta>
  5. <name>GoShopwareApp</name>
  6. <label>Go Shopware App</label>
  7. <author>Your name</author>
  8. <copyright>(c) 2026</copyright>
  9. <version>1.0.0</version>
  10. <license>MIT</license>
  11. </meta>
  12. <setup>
  13. <registrationUrl>http://localhost:3000/app/register</registrationUrl>
  14. <secret>my-app-secret-min-8-chars</secret>
  15. </setup>
  16. <permissions>
  17. <read>product</read>
  18. </permissions>
  19. <webhooks>
  20. <webhook name="appActivated" url="http://localhost:3000/app/activate" event="app.activated"/>
  21. <webhook name="appDeactivated" url="http://localhost:3000/app/deactivate" event="app.deactivated"/>
  22. <webhook name="appDeleted" url="http://localhost:3000/app/delete" event="app.deleted"/>
  23. <webhook name="productWritten" url="http://localhost:3000/app/webhook/product" event="product.written"/>
  24. </webhooks>
  25. </manifest>

Install the app with bin/console app:install --activate GoShopwareApp. Shopware calls the registration route, confirms with the shop's API credentials and sends the app.activated webhook. The SDK stores the shop, its secret and its credentials in SQLite and marks it active. Three responses with 204 No Content in the server log mean the shop is registered.

One detail worth knowing: each lifecycle route accepts only its own event. A valid app.activated payload replayed against /app/delete is rejected with 403, so a captured request cannot be turned into a different mutation.

Receiving a webhook

The manifest sends product.written to /app/webhook/product, a route we define ourselves. WebhookMiddleware verifies the signature, checks the timestamp, loads the shop and puts both the shop and the parsed event into the Gin context. For entity webhooks the payload is a list of changes.

  1. router.POST("/app/webhook/product",
  2. ginintegration.WebhookMiddleware(sdk, logger),
  3. func(c *gin.Context) {
  4. shop, _ := ginintegration.GetShop(c)
  5. event, _ := ginintegration.GetVerifiedEvent(c)
  6. var changes []webhook.EntityChange
  7. if err := event.Data.UnmarshalPayload(&changes); err != nil {
  8. c.Status(http.StatusBadRequest)
  9. return
  10. }
  11. for _, change := range changes {
  12. log.Printf("shop %s: %s %s %s", shop.ShopID, change.Operation, change.Entity, change.PrimaryKey)
  13. }
  14. c.Status(http.StatusNoContent)
  15. })

WebhookMiddleware takes the *app.App and a *slog.Logger; the core SDK itself never logs, only the integration does. To get the App, build it yourself and hand it to Configure instead of the individual fields. That is also what you need for the Admin API client later.

  1. sdk, err := app.New(app.Config{
  2. AppName: "GoShopwareApp",
  3. AppSecret: "my-app-secret-min-8-chars",
  4. ConfirmationURL: "http://localhost:3000/app/register/confirm",
  5. ShopStore: shopStore,
  6. })
  7. if err != nil {
  8. log.Fatal(err)
  9. }
  10. err = ginintegration.Configure(router, ginintegration.Config{App: sdk, Logger: logger})

Save a product in the Administration and the change shows up in the log.

Adding an action button

Action buttons let merchants trigger your app from the Administration. Add one to the product list in the manifest:

  1. <admin>
  2. <action-button action="syncProducts" entity="product" view="list" url="http://localhost:3000/action-button/sync">
  3. <label>Sync products</label>
  4. </action-button>
  5. </admin>

Shopware sends a signed JSON POST with the selected IDs. It is not a webhook event, so we verify it with the resolver's ResolveFromPost, which checks the signature and the shop state and leaves the body in place for our handler. Shopware expects the response to be signed as well, so we sign the body with the shop secret before writing it.

  1. router.POST("/action-button/sync", func(c *gin.Context) {
  2. shop, err := sdk.Resolver().ResolveFromPost(c.Request.Context(), c.Request)
  3. if err != nil {
  4. c.AbortWithStatus(http.StatusUnauthorized)
  5. return
  6. }
  7. var req struct {
  8. Data struct {
  9. IDs []string `json:"ids"`
  10. } `json:"data"`
  11. }
  12. if err := c.ShouldBindJSON(&req); err != nil {
  13. c.AbortWithStatus(http.StatusBadRequest)
  14. return
  15. }
  16. body, _ := json.Marshal(response.NotificationSuccess(
  17. fmt.Sprintf("%d products queued for sync", len(req.Data.IDs)),
  18. ))
  19. c.Header(signature.AppSignatureHeader, signature.ComputeSignature(body, shop.ShopSecret))
  20. c.Data(http.StatusOK, "application/json", body)
  21. })

The response package has builders for the other action button results too: reload the page, open a URL in a new tab or open a modal.

Calling the Admin API

Most apps need to read or write data after an event. NewClientForShop returns an Admin API client for the shop that sent the request. The client logs in with the shop's integration credentials on the first call, keeps the token, refreshes it before it expires and retries once on 401. All clients of a shop share one token. Inside the webhook handler from before:

  1. client, err := sdk.NewClientForShop(shop)
  2. if err != nil {
  3. c.Status(http.StatusInternalServerError)
  4. return
  5. }
  6. for _, change := range changes {
  7. resp, err := client.Get(c.Request.Context(), adminapi.Path("product", change.PrimaryKey))
  8. if adminapi.IsNotFound(err) {
  9. continue // deleted in the meantime
  10. }
  11. if err != nil {
  12. log.Printf("fetch product %s: %v", change.PrimaryKey, err)
  13. continue
  14. }
  15. var product struct {
  16. Data struct {
  17. Name string `json:"name"`
  18. } `json:"data"`
  19. }
  20. if err := resp.JSON(&product); err == nil {
  21. log.Printf("product %s is now %q", change.PrimaryKey, product.Data.Name)
  22. }
  23. }

Paths are relative to /api. adminapi.Path escapes each segment, so an ID taken from a payload cannot change the path. Reading products needs the <read>product</read> permission from the manifest.

The Admin API client on its own

This is where the Go SDK differs from the other two. The client lives in its own module, github.com/shopware/sdk-go/clients/adminapi, depends only on the standard library and golang.org/x/sync, and never imports the app SDK. If you are writing a CLI tool, a worker, or an integration that talks to a shop without being a Shopware app, install just the client:

  1. go get github.com/shopware/sdk-go/clients/adminapi

Create an integration in the Administration under Settings, System, Integrations and use its access key ID and secret:

  1. client, err := adminapi.NewClient(adminapi.Config{
  2. BaseURL: "https://shop.example.com",
  3. Credentials: adminapi.IntegrationCredentials{
  4. ClientID: os.Getenv("SHOPWARE_CLIENT_ID"),
  5. ClientSecret: os.Getenv("SHOPWARE_CLIENT_SECRET"),
  6. },
  7. })
  8. if err != nil {
  9. return err
  10. }
  11. resp, err := client.Get(ctx, "/_info/version")
  12. if err != nil {
  13. return err
  14. }
  15. var info struct {
  16. Version string `json:"version"`
  17. }
  18. if err := resp.JSON(&info); err != nil {
  19. return err
  20. }
  21. fmt.Println("Shopware", info.Version)

The same client supports admin user login with username and password, and your own Credentials implementation for other grants.

Tokens stay in memory by default; Config.TokenStorage moves them to Redis, Valkey or any key-value store so several processes share one token, with optional encryption. The app SDK uses exactly this client under the hood, so everything you learn here applies to apps as well.

The client is based on shopwareLabs/go-shopware-http-client, originally created by Friends of Shopware, and continues in the official SDK with the agreement of its owner.

Conclusion

In a few dozen lines we registered an app with a shop, handled a product webhook, added an action button and called the Admin API, with signatures, replay protection and token management handled by the SDK. We did not cover the DynamoDB store, the retry middleware for stores, the net/http setup without Gin, or payment and refund responses. The repository has runnable examples for Gin, plain net/http and an Administration extension, plus guides on getting started, usage, deployment and token storage.

The API reference is on pkg.go.dev.

If you build something with the SDK or miss a feature, open an issue or a discussion on GitHub.

Copied to clipboard