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

OperationMeaning
POST /ucp/quotesRequest a quote. Line items with optional per-unit asks and a comment. Returns the quote in state open.
GET /ucp/quotesThe 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}/counterCounter-offer with new per-unit asks and/or a comment. Valid from replied.
POST /ucp/quotes/{id}/acceptAccept the offer. Accepting is ordering: it places a real order and returns its reference.
POST /ucp/quotes/{id}/declineDecline the offer, optionally with a comment.

State machine

StateWhose turnWhat the buyer can do
draftnonenothing — a request that never reached the merchant
openmerchantpoll
in_reviewmerchantpoll
repliedbuyeraccept, counter, decline
change_requestedmerchantpoll
reopenmerchantpoll — the merchant reopened a declined quote; it returns to replied
accepted / declined / expired / withdrawn / cancellednoneterminal 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

SituationResultStatus
Capability disabled, not licensed, or the commercial Quote Management backend is unavailableunsupported capability — note the descriptor may still be in discovery501
Access token missing, invalid, expired, or revokedinvalid token401
Credential resolves to no customervalidation error naming the header422
Quote Management not enabled for the customer (customer_specific_features)validation error naming the feature422
Malformed request body or line itemsvalidation error422
Per-line price ask sent to a shop that cannot record onevalidation error telling you to send the ask as a comment422
Idempotency-Key missing where required, or reused for a different requestvalidation error / idempotency_conflict422 / 409
Quote id unknown or owned by another customernot found, without confirming existence404
Accepting from a non-actionable or expired stateerror from the commercial quote-to-order flow400

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.