com.shopware.quote
Vendor capability for the Shopware B2B Request-for-Quote flow. It is protocol-neutral,
so any UCP buyer agent can use it. com.shopware.* is Shopware's own
reverse-domain namespace; dev.ucp.* is reserved for the UCP governing body.
The machine-readable contract is the OpenAPI document advertised next to this page in
the capability descriptor (schema). This page is the prose companion, covering
the parts an implementer has to get right that a schema cannot express.
Discovery
The capability appears in /.well-known/ucp when the shop runs a UCP
surface and the merchant has enabled dev.ucp.common.identity_linking for
that sales channel — without it no token can be issued for quoting, so the descriptor is
suppressed rather than advertised as a dead end. Its descriptor carries the endpoint and
the schema URL:
"com.shopware.quote": [{
"version": "2026-04-08",
"spec": "https://shop.example/.well-known/ucp/specs/quote.html",
"schema": "https://shop.example/.well-known/ucp/schemas/quote.openapi.json",
"config": {
"endpoint": "https://shop.example/ucp/quotes",
"schema": "https://shop.example/.well-known/ucp/schemas/quote.openapi.json"
}
}]
If the entry is absent, the shop cannot quote: fall back to ordinary checkout, or tell the user that quoting is unavailable. Do not hard-code the endpoint; read it from the descriptor.
Present in discovery is not the same as usable. Discovery does not probe the commercial B2B Quote Management backend, so a shop without a licensed installation still advertises the capability and answers every call with 501. Treat the first 501 as the real availability answer and fall back exactly as you would for a missing entry.
Authorization
Quote operations act for a specific customer, so they need a credential that names one.
Obtain an OAuth 2.0 access token through the shop's
dev.ucp.common.identity_linking capability with the scope
com.shopware.quote:manage, and send it as
Authorization: Bearer <token>.
The token is the trust boundary. Its subject decides whose quotes are touched. Nothing in the request body can name a customer, and the agent never holds customer credentials. Revoking the link stops the token working immediately.
This shop does not yet enforce the com.shopware.quote:manage scope: request
it when linking identity, but no token can carry it until the identity-linking capability
supports issuing it. Authorization is by quote ownership instead — the token's subject
identifies the customer, and every operation is scoped to that customer's own quotes.
Requests also carry the usual UCP-Agent header, and an
Idempotency-Key on writes — required where the shop is configured to demand
it, and in every case a replay guard: the same key with the same request replays the
stored response (Idempotency-Replay: 1), a key reused for a different one
is a 409. A Shopware
sw-context-token is deliberately not accepted: it carries no OAuth
scopes and could reuse the customer's existing storefront cart.
Beyond authorization, the customer must have the commercial Quote Management feature enabled on their account. If not, calls fail with a validation error naming the missing feature. That is a merchant-side setting, not something an agent can resolve.
Operations
| Operation | Meaning |
|---|---|
POST /ucp/quotes | Request a quote. Line items with optional per-unit asks and a comment. Returns the quote in state open. |
GET /ucp/quotes | The customer's quotes, newest first, paged. Use it to rediscover a quote or poll several negotiations. |
GET /ucp/quotes/{id} | One quote: state, offered prices, totals, expiry, comments. |
POST /ucp/quotes/{id}/counter | Counter-offer with new per-unit asks and/or a comment. Valid from replied. |
POST /ucp/quotes/{id}/accept | Accept the offer. Accepting is ordering: it places a real order and returns its reference. |
POST /ucp/quotes/{id}/decline | Decline the offer, optionally with a comment. |
State machine
| State | Whose turn | What the buyer can do |
|---|---|---|
draft | none | nothing — a request that never reached the merchant |
open | merchant | poll |
in_review | merchant | poll |
replied | buyer | accept, counter, decline |
change_requested | merchant | poll |
reopen | merchant | poll — the merchant reopened a declined quote; it returns to replied |
accepted / declined / expired / withdrawn / cancelled | none | terminal for the buyer |
Only replied is buyer-actionable. Acting from any other state fails, so
drive the flow from the state you read rather than from what you expect.
declined is terminal for the buyer but not for the merchant: a reopened
quote reappears as reopen and becomes actionable again once it is back in
replied. The full transition table, both actors, is
x-state-machine in the schema.
Expiration
Every response exposes expiration_date, which may be null
before the merchant has replied. A quote can be flagged expired by a
background job, and accepting an expired quote fails. Read the date from the offer and
act before it.
Price semantics
All amounts are in the quote's currency.
totals.tax_status says whether totals.gross or
totals.net is the customer-facing authoritative amount. Every line item
price (unit_price, requested_unit_price, and the
requested_unit_price you send) is per unit. Your
requested price is an ask; the merchant's offer appears in unit_price and
the totals once the quote reaches replied.
Not every shop can record a per-line ask: the field and the route behind
requested_unit_price are missing on released commercial builds. There,
sending line_items with a requested price is a 422 naming the limitation,
and requested_unit_price reads back as null on every line.
Because a counter line item requires a requested price, a counter-offer on such a shop
is comment alone with no line_items — the merchant reads the
ask from the comment. Nothing in discovery reports this: send the counter you intended,
and on the 422 resend it as a comment.
Asynchronous negotiation
The merchant side answers out-of-band, possibly automated, possibly days later. Poll
GET /ucp/quotes/{id} (or the listing) for state changes; the recommended
interval is 300 seconds, published as x-polling-interval-seconds in the
schema. There is no webhook for quote state in this version.
Errors
| Situation | Result | Status |
|---|---|---|
| Capability disabled, not licensed, or the commercial Quote Management backend is unavailable | unsupported capability — note the descriptor may still be in discovery | 501 |
| Access token missing, invalid, expired, or revoked | invalid token | 401 |
| Credential resolves to no customer | validation error naming the header | 422 |
Quote Management not enabled for the customer (customer_specific_features) | validation error naming the feature | 422 |
| Malformed request body or line items | validation error | 422 |
| Per-line price ask sent to a shop that cannot record one | validation error telling you to send the ask as a comment | 422 |
Idempotency-Key missing where required, or reused for a different request | validation error / idempotency_conflict | 422 / 409 |
| Quote id unknown or owned by another customer | not found, without confirming existence | 404 |
| Accepting from a non-actionable or expired state | error from the commercial quote-to-order flow | 400 |
Every failure is the SDK's error envelope:
{"ucp": {"version", "status": "error"}, "messages": [{"code", "content", …}]}.
The status lives in ucp; there is no top-level status field.
Successful responses carry the same ucp envelope alongside the quote.
com.shopware.quote:manage is not in this table: it is requested but not
enforced (see Authorization above), so a missing scope never causes a failure on its own.
Versioning
The capability version tracks the UCP protocol version in the descriptor. Additive fields may appear without a version change; read defensively and ignore unknown fields.