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:
GET /.well-known/ucpShopware 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 field | Purpose |
|---|---|
ucp.version | Protocol version this Sales Channel currently supports. |
ucp.services | Transport endpoints such as REST /ucp/v1, MCP /ucp/mcp, A2A and Embedded Protocol. |
ucp.capabilities | Enabled commerce capabilities and extensions. |
ucp.payment_handlers | Payment handler descriptors offered by this Sales Channel. |
ucp.supported_versions | Optional version-pinned profile URLs for older protocol versions. |
signing_keys | Public 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:
UCP-Agent: profile="https://platform.example/profile.json"The REST/MCP ingress listener (UcpAgentRequestResolver) performs the handshake
before any controller runs:
- Resolve the Shopware Sales Channel from the request host.
- Load the UCP config for that Sales Channel.
- Parse
UCP-Agentand fetch the platform profile. - Apply URL-safety checks to the platform profile URL.
- Enforce
platform_allowlistwhen configured. - Negotiate the capability intersection.
- Verify HTTP Message Signatures according to
signature_policy. - Validate embedded-session or bearer token headers if present.
- Claim/replay idempotency for non-idempotent operations.
- Attach a
UcpRequestContextto 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:
| Protection | Behavior |
|---|---|
| HTTPS only | http:// is rejected outside dev/test. |
| Private IP blocking | Loopback, link-local, private and reserved ranges are rejected outside dev/test. |
| Metadata blocking | Cloud metadata hosts/IPs are rejected. |
| Redirect blocking | Redirects are disabled. |
| DNS rebinding defense | DNS result is validated and pinned for the actual HTTP fetch. |
| Userinfo blocking | URLs such as https://user:pass@host are rejected. |
| Size limit | Profile 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:
| Standard | Use |
|---|---|
| RFC 9421 HTTP Message Signatures | Inbound request verification and outbound order webhook signing. |
| RFC 9530 Content-Digest | Body digest verification for signed messages. |
| EC JWK keys | Public 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:
| Policy | Behavior |
|---|---|
strict | Missing or invalid signatures reject the request. Use this in production. |
log | Signature failures are logged and the request continues. Signals are not trusted. Useful for local simulator testing. |
off | Signature verification disabled. Local development only. |
5. Idempotency
State-changing REST routes use Idempotency-Key:
Idempotency-Key: 9b6d6c2e-...Shopware stores a fingerprint built from route name, method, path, sorted query string and raw body.
| Situation | Result |
|---|---|
| First request | A pending row is inserted and the controller runs. |
| Same key + same request after commit | Cached response is replayed with Idempotency-Replay: 1. |
| Same key + different request | HTTP 409 conflict. |
| Same key while first request is pending | HTTP 409 conflict. |
| Expired row | Row 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:
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:
POST /ucp/mcpSupported JSON-RPC methods:
| Method | Meaning |
|---|---|
initialize | MCP handshake. |
tools/list | Returns negotiated UCP tools. |
tools/call | Invokes a UCP operation. |
ping | Liveness. |
notifications/initialized | No-op acknowledgement. |
The same UCP handshake still applies. The platform profile can be supplied via
HTTP UCP-Agent or JSON-RPC metadata:
{
"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:
GET /.well-known/agent-card.jsonRuntime messages are sent to:
POST /ucp/a2aA2AMessageTranslator maps structured A2A DataPart actions to existing MCP
tools. For example:
| A2A action | UCP tool |
|---|---|
search_catalog | search_catalog |
add_to_cart | update_cart |
| checkout actions | checkout MCP tools |
Natural-language classification is intentionally outside this protocol layer.
9. Embedded Protocol Flow
Embedded URLs:
GET /ucp/embedded/cart/{cartId}?origin=https://host.example
GET /ucp/embedded/checkout/{cartId}?origin=https://host.exampleSecurity behavior:
originis required.- The iframe response sets
frame-ancestorsfor that origin. - The page uses
postMessagewithout wildcard targets. - Shopware issues a short-lived embedded session token.
- REST bridge calls must include
X-UCP-Embedded-Session. - 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:
GET /.well-known/oauth-authorization-server
GET /ucp/v1/.well-known/oauth-authorization-serverThe metadata advertises:
| Metadata field | Meaning |
|---|---|
issuer | Sales 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_supported | code |
grant_types_supported | authorization_code, refresh_token |
code_challenge_methods_supported | S256 |
authorization_response_iss_parameter_supported | true, for mix-up mitigation. |
10.2 Authorization Code + PKCE
Flow:
- Platform builds an authorization URL with
client_id,redirect_uri, scopes,state,code_challengeandcode_challenge_method=S256. - Buyer opens
/ucp/v1/oauth/authorize. - Shopware validates the request through League OAuth2 Server.
- If the buyer is not logged in, Shopware redirects to storefront login.
- If logged in, Shopware renders a consent page.
- Consent POST is protected by a short-lived HMAC consent-ticket cookie and hidden CSRF token.
- If approved, League issues an authorization code and redirects back to the platform.
- Shopware appends
iss=<issuer>to the redirect per RFC 9207. - Platform exchanges the code at
/ucp/v1/oauth/token. - Token endpoint validates PKCE and client authentication.
- Shopware returns an ES256/ES384 JWT access token and optional refresh token.
10.3 Client Authentication
Supported token endpoint auth methods:
| Method | Behavior |
|---|---|
none | Public clients, still requiring PKCE. |
client_secret_post | Confidential client secret in form body, handled by League. |
private_key_jwt | Validated by ClientAuthenticator; requires jti replay protection. |
tls_client_auth | Validates server-injected mTLS subject DN. Client-supplied X-SSL-* headers are not trusted. |
10.4 Using Bearer Tokens
When a UCP request contains:
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:
| Scope | Typical use |
|---|---|
dev.ucp.shopping.cart:manage | Mutating cart operations. |
dev.ucp.shopping.order:read | Reading order data. |
dev.ucp.shopping.order:manage | Future order management operations. |
11. AP2 Mandate Flow
AP2 lives in custom/plugins/SwagUcpAp2Mandates.
When dev.ucp.shopping.ap2_mandate is in the negotiated intersection:
- Every checkout response receives
ap2.merchant_authorization. - The merchant authorization is a detached JWS over the JCS-canonicalized
checkout response without the
ap2field. - The platform must verify the merchant authorization before presenting the checkout to the buyer.
- On
complete_checkout, the platform must sendap2.checkout_mandate. - The payment mandate is read from
ap2.payment_mandateor the selected payment credential token. - Shopware verifies mandate signatures against the negotiated platform profile keys.
- Shopware pins
issto the negotiated platform profile URI. - Shopware checks
aud,exp, TTL, replay, subject binding, payment/checkout cross-binding and cart intent binding. - Only then is
CartOrderRoute::order()called.
Supported AP2 wire formats:
| Format | Use |
|---|---|
| Compact JWS | Simulator/debug compatibility. |
| SD-JWT VC + KB-JWT | Production 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:
- UCP request negotiation records the platform profile and active capabilities.
- On order state changes,
OrderWebhookPublisherresolves the platform profile. - It extracts
webhook_urlfrom the order capability config. - The webhook URL passes URL-safety validation.
- Shopware builds an order payload.
- Shopware signs the POST with RFC 9421 and includes content digest headers.
Webhook-IdandWebhook-Timestampare included for Standard Webhooks-style replay/deduplication.
13. Conformance Test Mode
Official upstream conformance tests are supported through an explicit non-production bridge:
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.