Reference

Runtime flows

This document explains how Shopware's UCP adapter talks to platform agents:

UCP Runtime Flows

This document explains how Shopware's UCP adapter talks to platform agents: discovery, handshake, signatures, idempotency, OAuth identity linking, transports, AP2 mandates, webhooks and the test-only conformance bridge.

1. Discovery

The first request from a platform is usually:

http
GET /.well-known/ucp

Shopware resolves the request host to a sales_channel_domain, then loads the matching ucp_sales_channel_config row. The response is built by UcpProfileBuilder.

The profile tells the platform:

Profile fieldPurpose
ucp.versionProtocol version this Sales Channel currently supports.
ucp.servicesTransport endpoints such as REST /ucp/v1, MCP /ucp/mcp, A2A and Embedded Protocol.
ucp.capabilitiesEnabled commerce capabilities and extensions.
ucp.payment_handlersPayment handler descriptors offered by this Sales Channel.
ucp.supported_versionsOptional version-pinned profile URLs for older protocol versions.
signing_keysPublic EC keys used by platforms to verify Shopware signatures.

The profile is per Sales Channel domain. Two storefront domains can therefore publish different UCP capability sets and signing keys.

2. Request Handshake

For UCP runtime requests, the platform identifies itself with UCP-Agent:

http
UCP-Agent: profile="https://platform.example/profile.json"

The REST/MCP ingress listener (UcpAgentRequestResolver) performs the handshake before any controller runs:

  1. Resolve the Shopware Sales Channel from the request host.
  2. Load the UCP config for that Sales Channel.
  3. Parse UCP-Agent and fetch the platform profile.
  4. Apply URL-safety checks to the platform profile URL.
  5. Enforce platform_allowlist when configured.
  6. Negotiate the capability intersection.
  7. Verify HTTP Message Signatures according to signature_policy.
  8. Validate embedded-session or bearer token headers if present.
  9. Claim/replay idempotency for non-idempotent operations.
  10. Attach a UcpRequestContext to the Symfony request.

If the capability intersection is empty, the request is rejected before it can reach a commerce controller.

3. Profile Fetching And SSRF Defense

Platform profile fetching is handled by PlatformProfileFetcher and UrlSafetyValidator.

Production behavior:

ProtectionBehavior
HTTPS onlyhttp:// is rejected outside dev/test.
Private IP blockingLoopback, link-local, private and reserved ranges are rejected outside dev/test.
Metadata blockingCloud metadata hosts/IPs are rejected.
Redirect blockingRedirects are disabled.
DNS rebinding defenseDNS result is validated and pinned for the actual HTTP fetch.
Userinfo blockingURLs such as https://user:pass@host are rejected.
Size limitProfile response is capped at 256 KiB.

The Docker-specific localhost to host.docker.internal fallback exists only when explicit UCP_CONFORMANCE_MODE=1 is enabled and is not part of production behavior.

4. HTTP Signatures And Signing Keys

Shopware uses:

StandardUse
RFC 9421 HTTP Message SignaturesInbound request verification and outbound order webhook signing.
RFC 9530 Content-DigestBody digest verification for signed messages.
EC JWK keysPublic keys are published in /.well-known/ucp.

Sales Channel signing keys are managed in the UCP admin module. The private key is encrypted at rest; the public JWK is published.

Inbound behavior is controlled by signature_policy:

PolicyBehavior
strictMissing or invalid signatures reject the request. Use this in production.
logSignature failures are logged and the request continues. Signals are not trusted. Useful for local simulator testing.
offSignature verification disabled. Local development only.

5. Idempotency

State-changing REST routes use Idempotency-Key:

http
Idempotency-Key: 9b6d6c2e-...

Shopware stores a fingerprint built from route name, method, path, sorted query string and raw body.

SituationResult
First requestA pending row is inserted and the controller runs.
Same key + same request after commitCached response is replayed with Idempotency-Replay: 1.
Same key + different requestHTTP 409 conflict.
Same key while first request is pendingHTTP 409 conflict.
Expired rowRow is deleted and claimed atomically again.

MCP mutating tools carry the same protection through _meta.idempotency_key or the Idempotency-Key header.

6. REST Flow

REST endpoints live below /ucp/v1.

Typical checkout flow:

text
Platform                         Shopware
   | GET /.well-known/ucp            |
   |-------------------------------->|
   | profile + signing_keys          |
   |<--------------------------------|
   | POST /catalog/search            |
   |-------------------------------->|
   | products                        |
   |<--------------------------------|
   | POST /carts                     |
   |-------------------------------->|
   | cart id                         |
   |<--------------------------------|
   | POST /checkout-sessions         |
   |-------------------------------->|
   | checkout id, totals, payment    |
   |<--------------------------------|
   | POST /checkout-sessions/{id}/complete
   |-------------------------------->|
   | completed checkout + order id   |
   |<--------------------------------|

REST responses are wrapped by UcpResponseEnvelopeListener. The envelope is operation-filtered, so a cart response advertises cart-related capabilities, while checkout responses also include payment handlers.

7. MCP Flow

MCP uses JSON-RPC over:

http
POST /ucp/mcp

Supported JSON-RPC methods:

MethodMeaning
initializeMCP handshake.
tools/listReturns negotiated UCP tools.
tools/callInvokes a UCP operation.
pingLiveness.
notifications/initializedNo-op acknowledgement.

The same UCP handshake still applies. The platform profile can be supplied via HTTP UCP-Agent or JSON-RPC metadata:

json
{
  "params": {
    "_meta": {
      "ucp-agent": {
        "profile": "https://platform.example/profile.json"
      }
    }
  }
}

MCP tools mirror the REST capability surface, including search_catalog, lookup_catalog, get_product, cart tools, checkout tools and get_order.

8. A2A Flow

A2A discovery starts with:

http
GET /.well-known/agent-card.json

Runtime messages are sent to:

http
POST /ucp/a2a

A2AMessageTranslator maps structured A2A DataPart actions to existing MCP tools. For example:

A2A actionUCP tool
search_catalogsearch_catalog
add_to_cartupdate_cart
checkout actionscheckout MCP tools

Natural-language classification is intentionally outside this protocol layer.

9. Embedded Protocol Flow

Embedded URLs:

http
GET /ucp/embedded/cart/{cartId}?origin=https://host.example
GET /ucp/embedded/checkout/{cartId}?origin=https://host.example

Security behavior:

  1. origin is required.
  2. The iframe response sets frame-ancestors for that origin.
  3. The page uses postMessage without wildcard targets.
  4. Shopware issues a short-lived embedded session token.
  5. REST bridge calls must include X-UCP-Embedded-Session.
  6. The embedded session is bound to cart, Sales Channel and host origin.

10. OAuth Identity Linking

OAuth is only used when the dev.ucp.common.identity_linking capability is enabled and the platform wants user-linked access. Anonymous shopping remains allowed without OAuth.

10.1 Discovery

Authorization Server Metadata is published at:

http
GET /.well-known/oauth-authorization-server
GET /ucp/v1/.well-known/oauth-authorization-server

The metadata advertises:

Metadata fieldMeaning
issuerSales Channel domain URL.
authorization_endpoint/ucp/v1/oauth/authorize
token_endpoint/ucp/v1/oauth/token
jwks_uri/.well-known/ucp; access-token keys are published in the UCP profile.
response_types_supportedcode
grant_types_supportedauthorization_code, refresh_token
code_challenge_methods_supportedS256
authorization_response_iss_parameter_supportedtrue, for mix-up mitigation.

10.2 Authorization Code + PKCE

Flow:

  1. Platform builds an authorization URL with client_id, redirect_uri, scopes, state, code_challenge and code_challenge_method=S256.
  2. Buyer opens /ucp/v1/oauth/authorize.
  3. Shopware validates the request through League OAuth2 Server.
  4. If the buyer is not logged in, Shopware redirects to storefront login.
  5. If logged in, Shopware renders a consent page.
  6. Consent POST is protected by a short-lived HMAC consent-ticket cookie and hidden CSRF token.
  7. If approved, League issues an authorization code and redirects back to the platform.
  8. Shopware appends iss=<issuer> to the redirect per RFC 9207.
  9. Platform exchanges the code at /ucp/v1/oauth/token.
  10. Token endpoint validates PKCE and client authentication.
  11. Shopware returns an ES256/ES384 JWT access token and optional refresh token.

10.3 Client Authentication

Supported token endpoint auth methods:

MethodBehavior
nonePublic clients, still requiring PKCE.
client_secret_postConfidential client secret in form body, handled by League.
private_key_jwtValidated by ClientAuthenticator; requires jti replay protection.
tls_client_authValidates server-injected mTLS subject DN. Client-supplied X-SSL-* headers are not trusted.

10.4 Using Bearer Tokens

When a UCP request contains:

http
Authorization: Bearer <access-token>

UcpAccessTokenAuthenticator validates it fail-closed. Invalid bearer tokens reject the request; they do not fall back to anonymous access.

Controllers then use UcpScopeGuard to require scopes such as:

ScopeTypical use
dev.ucp.shopping.cart:manageMutating cart operations.
dev.ucp.shopping.order:readReading order data.
dev.ucp.shopping.order:manageFuture order management operations.

11. AP2 Mandate Flow

AP2 lives in custom/plugins/SwagUcpAp2Mandates.

When dev.ucp.shopping.ap2_mandate is in the negotiated intersection:

  1. Every checkout response receives ap2.merchant_authorization.
  2. The merchant authorization is a detached JWS over the JCS-canonicalized checkout response without the ap2 field.
  3. The platform must verify the merchant authorization before presenting the checkout to the buyer.
  4. On complete_checkout, the platform must send ap2.checkout_mandate.
  5. The payment mandate is read from ap2.payment_mandate or the selected payment credential token.
  6. Shopware verifies mandate signatures against the negotiated platform profile keys.
  7. Shopware pins iss to the negotiated platform profile URI.
  8. Shopware checks aud, exp, TTL, replay, subject binding, payment/checkout cross-binding and cart intent binding.
  9. Only then is CartOrderRoute::order() called.

Supported AP2 wire formats:

FormatUse
Compact JWSSimulator/debug compatibility.
SD-JWT VC + KB-JWTProduction AP2 format with selective disclosures and key binding.

12. Order Webhook Flow

For platforms that advertise dev.ucp.shopping.order.config.webhook_url, Shopware can send order lifecycle webhooks.

Flow:

  1. UCP request negotiation records the platform profile and active capabilities.
  2. On order state changes, OrderWebhookPublisher resolves the platform profile.
  3. It extracts webhook_url from the order capability config.
  4. The webhook URL passes URL-safety validation.
  5. Shopware builds an order payload.
  6. Shopware signs the POST with RFC 9421 and includes content digest headers.
  7. Webhook-Id and Webhook-Timestamp are included for Standard Webhooks-style replay/deduplication.

13. Conformance Test Mode

Official upstream conformance tests are supported through an explicit non-production bridge:

bash
UCP_CONFORMANCE_MODE=1
UCP_SIMULATION_SECRET=<secret>

This mode enables fixture-specific behavior required by the upstream Python suite, including:

  • request-signature: test
  • placeholder UCP-Agent: profile="..."
  • the simulation shipping endpoint
  • flower-shop fixture seeding through bin/console ucp:conformance:seed
  • deterministic test discounts and fulfillment test helpers
  • local Docker host fallback for the suite's mock agent and webhook servers

Conformance mode is disabled in production and must not be used as runtime business logic.