Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions src/Resources/schema/quote.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "com.shopware.quote",
"version": "2026-04-08",
"description": "Buyer-facing B2B Request-for-Quote capability for Shopware shops. Protocol-neutral: any UCP buyer agent can use it.\n\n## Authentication and authorization\n\nQuote operations act on behalf of a customer. Obtain an OAuth 2.0 access token through the shop's `dev.ucp.common.identity_linking` capability, then send it as `Authorization: Bearer <token>`. The access token subject is the trust boundary: the customer's consent decides whose quotes are touched, and nothing in the request body can select a customer. An unscoped Shopware context token is not accepted. Requests also carry the standard `UCP-Agent` header and a request signature when the shop's signature policy requires it.\n\nWhen `/.well-known/oauth-authorization-server` lists `com.shopware.quote:manage` in `scopes_supported`, the shop enforces scopes: every quote operation requires `com.shopware.quote:manage`, and `acceptQuote` additionally requires `dev.ucp.shopping.order:manage`, because accepting places an order. Request both when linking the account. A token missing a required scope gets a 403 naming the missing scopes; link the account again, requesting them. A shop that does not list the quote scope cannot issue it, so it does not check scopes. Authorization is also by quote ownership, on every shop: the token's subject identifies the customer, and every operation is scoped to that customer's own quotes. A request whose token carries no linked customer fails with a validation error rather than succeeding unscoped.\n\nThe capability is advertised in discovery whenever the shop has a UCP surface and the `dev.ucp.common.identity_linking` capability is enabled for the sales channel; a shop that restricts its capabilities and omits identity linking publishes no quote descriptor at all, because a capability nobody can obtain a token for is a dead end. Being advertised is not being routable: discovery does not probe the commercial backend, so a shop without a licensed Quote Management installation advertises the capability and fails every request as unsupported. Treat a 501 as the real availability answer. The customer must additionally have the Quote Management feature enabled (`customer_specific_features`). Unknown and foreign quote ids both return not found, without confirming existence.\n\n## State machine\n\n`open` and `in_review` are the merchant's turn. `replied` is the buyer's turn: accept, counter, or decline. `change_requested` returns the turn to the merchant. Accepted, declined, expired, withdrawn, and cancelled quotes are terminal for the buyer; a merchant may reopen or extend a quote as documented in `x-state-machine`.\n\n## Expiration\n\nOffers carry `expiration_date` (ISO 8601, always present in responses, and possibly null before the merchant replies). Agents must read it and act before it.\n\n## Price semantics\n\nAll amounts are in the quote's `currency`. `totals.tax_status` states whether `totals.gross` or `totals.net` is authoritative. Line item prices and requested prices are **per unit**.\n\nNot every shop can record a per-line ask: the field and the route behind `requested_unit_price` are absent on released commercial builds. Where they are, sending `line_items` with a `requested_unit_price` fails with a validation error naming the limitation, and `requested_unit_price` is always null on reads. Since a counter line item requires a `requested_unit_price`, on those shops a counter-offer must be sent as `comment` alone, with no `line_items` — the merchant reads the ask from the comment. Probe once with the counter you intended and fall back to a comment on the 422; nothing in discovery reports this.\n\n## Idempotency\n\nEvery mutating request accepts `Idempotency-Key`. Shops configured to require it reject a mutating request without one. A repeated key with the same request fingerprint replays the stored response with `Idempotency-Replay: 1`; a key reused for a different request, or for a response too large to have been stored, is a 409.\n\n## Async negotiation\n\nThe merchant responds out-of-band. Agents poll `GET /ucp/quotes/{id}` for state changes; the recommended interval is 300 seconds. Webhooks are not part of this capability version.",
"description": "Buyer-facing B2B Request-for-Quote capability for Shopware shops. Protocol-neutral: any UCP buyer agent can use it.\n\n## Authentication and authorization\n\nQuote operations act on behalf of a customer. Obtain an OAuth 2.0 access token through the shop's `dev.ucp.common.identity_linking` capability, then send it as `Authorization: Bearer <token>`. The access token subject is the trust boundary: the customer's consent decides whose quotes are touched, and nothing in the request body can select a customer. An unscoped Shopware context token is not accepted. Requests also carry the standard `UCP-Agent` header and a request signature when the shop's signature policy requires it.\n\nWhen `/.well-known/oauth-authorization-server` lists `com.shopware.quote:manage` in `scopes_supported`, the shop enforces scopes: every quote operation requires `com.shopware.quote:manage`, and `acceptQuote` additionally requires `dev.ucp.shopping.order:manage`, because accepting places an order. Request both when linking the account. A token missing a required scope gets a 403 naming the missing scopes; link the account again, requesting them. A shop that does not list the quote scope cannot issue it, so it does not check scopes. Authorization is also by quote ownership, on every shop: the token's subject identifies the customer, and every operation is scoped to that customer's own quotes. A request whose token carries no linked customer fails with a validation error rather than succeeding unscoped.\n\nThe capability is advertised in discovery whenever the shop has a UCP surface and the `dev.ucp.common.identity_linking` capability is enabled for the sales channel; a shop that restricts its capabilities and omits identity linking publishes no quote descriptor at all, because a capability nobody can obtain a token for is a dead end. Being advertised is not being usable: discovery does not check the commercial Quote Management backend. A shop without it serves no quote routes, so every request is a 404; a shop that has it but no license fails every request as unsupported (501). Treat either as the real availability answer. A 404 on the `/ucp/quotes` collection can only mean this; on a quote id it can also mean the quote is not the customer's. To request, counter, accept or decline, the customer must additionally have the Quote Management feature enabled (`customer_specific_features`); reads are not checked. Unknown and foreign quote ids both return not found, without confirming existence.\n\n## State machine\n\n`open` and `in_review` are the merchant's turn. `replied` is the buyer's turn: accept, counter, or decline. `change_requested` returns the turn to the merchant. Accepted, declined, expired, withdrawn, and cancelled quotes are terminal for the buyer; a merchant may reopen or extend a quote as documented in `x-state-machine`.\n\n## Expiration\n\nOffers carry `expiration_date` (ISO 8601, always present in responses, and possibly null before the merchant replies). Agents must read it and act before it.\n\n## Price semantics\n\nAll amounts are in the quote's `currency`. `totals.tax_status` states whether `totals.gross` or `totals.net` is authoritative. Line item prices and requested prices are **per unit**.\n\nNot every shop can record a per-line ask: the field and the route behind `requested_unit_price` are absent on released commercial builds. Where they are absent, sending `line_items` with a `requested_unit_price` fails with a validation error naming the limitation, and `requested_unit_price` is always null on reads. Since a counter line item requires a `requested_unit_price`, on those shops a counter-offer must be sent as `comment` alone, with no `line_items` — the merchant reads the ask from the comment. Probe once with the counter you intended and fall back to a comment on the 422; nothing in discovery reports this.\n\n## Idempotency\n\nEvery mutating request accepts `Idempotency-Key`. Shops configured to require it reject a mutating request without one. A repeated key with the same request fingerprint replays the stored response with `Idempotency-Replay: 1`; a key that is still being processed, or reused for a different request, or for a response too large to have been stored, is a 409.\n\n## Async negotiation\n\nThe merchant responds out-of-band. Agents poll `GET /ucp/quotes/{id}` for state changes; the recommended interval is 300 seconds. Webhooks are not part of this capability version.",
"x-capability": "com.shopware.quote"
},
"x-polling-interval-seconds": 300,
Expand Down Expand Up @@ -194,6 +194,9 @@
"409": {
"$ref": "#/components/responses/Conflict"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"501": {
"$ref": "#/components/responses/Unsupported"
}
Expand All @@ -213,7 +216,7 @@
"maximum": 50,
"default": 25
},
"description": "Page size; values above 50 are clamped."
"description": "Page size; values above 50 are clamped. A non-integer value is a 400."
},
{
"name": "page",
Expand All @@ -223,7 +226,8 @@
"type": "integer",
"minimum": 1,
"default": 1
}
},
"description": "A non-integer value is a 400."
}
],
"responses": {
Expand All @@ -246,6 +250,9 @@
"422": {
"$ref": "#/components/responses/UnprocessableEntity"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"501": {
"$ref": "#/components/responses/Unsupported"
}
Expand Down Expand Up @@ -379,7 +386,7 @@
}
},
"400": {
"description": "Not acceptable in current state or expired (code CHECKOUT__QUOTE_CANNOT_PLACE_ORDER)",
"description": "Not acceptable in current state or expired. The message `code` is `invalid_request`; `content` carries the reason from the commercial quote-to-order flow.",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -497,7 +504,7 @@
"schema": {
"type": "string"
},
"description": "Replay guard for a mutating request. Required by shops whose signature policy demands it; a replayed response carries `Idempotency-Replay: 1`."
"description": "Replay guard for a mutating request. Required by shops configured to demand it; a replayed response carries `Idempotency-Replay: 1`."
}
},
"responses": {
Expand All @@ -512,7 +519,7 @@
}
},
"UnprocessableEntity": {
"description": "Invalid payload, per-line price asks sent to a shop that cannot record them, the access token does not resolve to a customer, or Quote Management is not enabled for the customer (`customer_specific_features` must contain `{\"QUOTE_MANAGEMENT\": true}`)",
"description": "Invalid payload, per-line price asks sent to a shop that cannot record them, the access token does not resolve to a customer, or, on a request, counter, accept or decline, Quote Management is not enabled for the customer (`customer_specific_features` must contain `{\"QUOTE_MANAGEMENT\": true}`)",
"content": {
"application/json": {
"schema": {
Expand All @@ -522,7 +529,7 @@
}
},
"NotFound": {
"description": "Quote unknown or owned by another customer, without confirming which",
"description": "Quote unknown or owned by another customer, without confirming which. On any path, also a shop without the commercial Quote Management backend, which serves no quote routes.",
"content": {
"application/json": {
"schema": {
Expand All @@ -532,7 +539,7 @@
}
},
"Unsupported": {
"description": "The quote capability is disabled, not licensed, or the commercial Quote Management backend is unavailable",
"description": "The commercial Quote Management backend is installed but not licensed, or a service the capability needs is unavailable",
"content": {
"application/json": {
"schema": {
Expand All @@ -542,7 +549,7 @@
}
},
"Conflict": {
"description": "Idempotency key reused for a different request, or for a response that is no longer replayable (code `idempotency_conflict`)",
"description": "Idempotency key still being processed, reused for a different request, or for a response that is no longer replayable (code `idempotency_conflict`)",
"content": {
"application/json": {
"schema": {
Expand All @@ -552,7 +559,7 @@
}
},
"InsufficientScope": {
"description": "insufficient_scope: the access token is valid but lacks a scope this operation requires. Only on shops that list com.shopware.quote:manage in scopes_supported. Link the account again, requesting the scopes the message names.",
"description": "The access token is valid but lacks a scope this operation requires. The message `code` is `invalid_request`; its `content` starts with `insufficient_scope:` and names the missing scopes. Only on shops that list com.shopware.quote:manage in scopes_supported. Link the account again, requesting those scopes.",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -787,7 +794,7 @@
},
"order": {
"type": "object",
"description": "Present only after acceptance",
"description": "Present only in the acceptQuote response (and its idempotent replay); later reads of an accepted quote do not carry it.",
"required": [
"id"
],
Expand All @@ -805,21 +812,18 @@
},
"a2cn_session_id": {
"type": "string",
"description": "Present only when the quote was created with an A2CN session stamped onto it. The buyer's negotiation session id for POST /a2cn/sessions/{sessionId}/messages; derivable from the quote id, so its absence is not an error."
"description": "Present only in the requestQuote response, when an A2CN session was stamped onto the new quote; later reads do not carry it. The buyer's negotiation session id for POST /a2cn/sessions/{sessionId}/messages; derivable from the quote id, so its absence is not an error."
},
"ucp": {
"type": "object",
"description": "Protocol envelope added to every successful response; `capabilities` appears once an agent has negotiated.",
"description": "Protocol envelope added to every successful response.",
"properties": {
"version": {
"type": "string"
},
"status": {
"type": "string",
"const": "success"
},
"capabilities": {
"type": "object"
}
}
}
Expand Down Expand Up @@ -861,9 +865,10 @@
"type": "string",
"description": "Machine-readable error code",
"examples": [
"validation_error",
"resource_not_found",
"unsupported_capability"
"invalid_request",
"not_found",
"idempotency_conflict",
"capability_unsupported"
]
},
"content": {
Expand Down
Loading
Loading