{
  "openapi": "3.1.0",
  "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 with the `com.shopware.quote:manage` scope, 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\nThe `com.shopware.quote:manage` scope is requested when obtaining the token, but this shop does not yet enforce it: no token can carry it until the identity-linking capability supports issuing it. Authorization today is by quote ownership instead — 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.",
    "x-capability": "com.shopware.quote"
  },
  "x-polling-interval-seconds": 300,
  "x-state-machine": {
    "states": {
      "draft": {
        "actor": "none",
        "buyer_actions": [],
        "note": "A request that never reached the merchant (the cart-to-quote step ran but customer_send did not). No buyer operation advances it; it can only be loaded or listed."
      },
      "open": {
        "actor": "merchant",
        "buyer_actions": []
      },
      "in_review": {
        "actor": "merchant",
        "buyer_actions": []
      },
      "replied": {
        "actor": "buyer",
        "buyer_actions": [
          "accept",
          "counter",
          "decline"
        ]
      },
      "change_requested": {
        "actor": "merchant",
        "buyer_actions": []
      },
      "accepted": {
        "actor": "none",
        "terminal": true,
        "buyer_actions": []
      },
      "declined": {
        "actor": "none",
        "terminal": true,
        "buyer_actions": []
      },
      "expired": {
        "actor": "merchant",
        "terminal": true,
        "buyer_actions": [],
        "note": "merchant may extend expiration back to replied"
      },
      "withdrawn": {
        "actor": "none",
        "terminal": true,
        "buyer_actions": []
      },
      "cancelled": {
        "actor": "none",
        "terminal": true,
        "buyer_actions": []
      },
      "reopen": {
        "actor": "merchant",
        "buyer_actions": [],
        "note": "merchant reopened a previously declined quote. Its only exit is `admin_resend`, the same action that leaves `change_requested`."
      }
    },
    "transitions": [
      {
        "from": "draft",
        "action": "customer_send",
        "to": "open",
        "actor": "buyer",
        "note": "performed automatically inside POST /ucp/quotes, so that endpoint never returns a draft; a draft can still be loaded or listed later if this step never ran"
      },
      {
        "from": "open",
        "action": "process",
        "to": "in_review",
        "actor": "merchant"
      },
      {
        "from": "open",
        "action": "sent",
        "to": "replied",
        "actor": "merchant"
      },
      {
        "from": "in_review",
        "action": "sent",
        "to": "replied",
        "actor": "merchant"
      },
      {
        "from": "replied",
        "action": "accept",
        "to": "accepted",
        "actor": "buyer",
        "endpoint": "POST /ucp/quotes/{id}/accept"
      },
      {
        "from": "replied",
        "action": "decline",
        "to": "declined",
        "actor": "buyer",
        "endpoint": "POST /ucp/quotes/{id}/decline"
      },
      {
        "from": "replied",
        "action": "request_change",
        "to": "change_requested",
        "actor": "buyer",
        "endpoint": "POST /ucp/quotes/{id}/counter"
      },
      {
        "from": "replied",
        "action": "expire",
        "to": "expired",
        "actor": "system"
      },
      {
        "from": "change_requested",
        "action": "admin_resend",
        "to": "replied",
        "actor": "merchant"
      },
      {
        "from": "reopen",
        "action": "admin_resend",
        "to": "replied",
        "actor": "merchant"
      },
      {
        "from": "expired",
        "action": "admin_extend_expiration",
        "to": "replied",
        "actor": "merchant"
      },
      {
        "from": "declined",
        "action": "reopen",
        "to": "reopen",
        "actor": "merchant"
      }
    ]
  },
  "security": [
    {
      "bearerAuth": [],
      "ucpAgent": []
    }
  ],
  "paths": {
    "/ucp/quotes": {
      "post": {
        "operationId": "requestQuote",
        "summary": "Request a quote (RFQ). Creates and submits the request in one call; the returned quote is in state `open`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Quote created and submitted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      },
      "get": {
        "operationId": "listQuotes",
        "summary": "The customer's quotes, newest first. Only quotes owned by the customer the credential resolves to are listed. Use this to rediscover a quote whose id was not stored, or to poll several negotiations at once.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 25
            },
            "description": "Page size; values above 50 are clamped."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of quotes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      }
    },
    "/ucp/quotes/{id}": {
      "get": {
        "operationId": "getQuote",
        "summary": "Read a quote snapshot (state, line items with offered prices, totals, expiration_date, comments). Only quotes owned by the resolved customer are visible; anything else is 404.",
        "parameters": [
          {
            "$ref": "#/components/parameters/quoteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Quote snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      }
    },
    "/ucp/quotes/{id}/counter": {
      "post": {
        "operationId": "counterQuote",
        "summary": "Counter-offer: new per-line requested unit prices and/or a comment. Valid only in state `replied`; transitions to `change_requested`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/quoteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CounterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated quote (state change_requested)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      }
    },
    "/ucp/quotes/{id}/accept": {
      "post": {
        "operationId": "acceptQuote",
        "summary": "Accept the offer. Accepting IS ordering: executes the quote-to-order flow and returns the order reference. Valid only in state `replied` and before expiration_date.",
        "parameters": [
          {
            "$ref": "#/components/parameters/quoteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order placed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptResponse"
                }
              }
            }
          },
          "400": {
            "description": "Not acceptable in current state or expired (code CHECKOUT__QUOTE_CANNOT_PLACE_ORDER)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      }
    },
    "/ucp/quotes/{id}/decline": {
      "post": {
        "operationId": "declineQuote",
        "summary": "Decline the offer with an optional comment. Valid only in state `replied`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/quoteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeclineRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined quote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/Unsupported"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "OAuth 2.0",
        "description": "Identity-linking access token. Its subject identifies the customer, which is what this shop authorizes on today; the com.shopware.quote:manage scope is requested but not yet enforced."
      },
      "ucpAgent": {
        "type": "apiKey",
        "in": "header",
        "name": "UCP-Agent",
        "description": "Standard UCP agent identification header (profile URL). Signature requirements follow the sales channel signature policy."
      }
    },
    "parameters": {
      "quoteId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{32}$"
        },
        "description": "Quote id (UUID hex). Ids are unguessable but treated as secrets of the owning customer: foreign ids return 404."
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "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`."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Access token missing, invalid, expired, or revoked",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "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}`)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Quote unknown or owned by another customer, without confirming which",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unsupported": {
        "description": "The quote capability is disabled, not licensed, or the commercial Quote Management backend is unavailable",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency key reused for a different request, or for a response that is no longer replayable (code `idempotency_conflict`)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "QuoteRequest": {
        "type": "object",
        "required": [
          "line_items"
        ],
        "properties": {
          "line_items": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "description": "product_id identifies the Shopware product to quote",
              "properties": {
                "product_id": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer",
                  "minimum": 1
                },
                "requested_unit_price": {
                  "type": "number",
                  "description": "Buyer's asking price per unit, in the shop's customer-facing currency"
                }
              },
              "required": [
                "product_id",
                "quantity"
              ]
            }
          },
          "comment": {
            "type": "string"
          }
        }
      },
      "CounterRequest": {
        "type": "object",
        "properties": {
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Reference an existing quote line by id (preferred) or product_id",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Quote line item id from GET /ucp/quotes/{id}"
                },
                "product_id": {
                  "type": "string"
                },
                "requested_unit_price": {
                  "type": "number",
                  "description": "New asking price per unit"
                }
              },
              "required": [
                "requested_unit_price"
              ]
            },
            "description": "Omit entirely on a shop that cannot record per-line asks — sending any line item there is a 422, and the ask belongs in `comment` instead."
          },
          "comment": {
            "type": "string"
          }
        }
      },
      "DeclineRequest": {
        "type": "object",
        "properties": {
          "comment": {
            "type": "string"
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "id",
          "state",
          "expiration_date",
          "totals",
          "line_items"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "quote_number": {
            "type": "string"
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "draft",
              "open",
              "in_review",
              "replied",
              "change_requested",
              "accepted",
              "declined",
              "expired",
              "withdrawn",
              "cancelled",
              "reopen",
              null
            ],
            "description": "Null only if the merchant's state machine state could not be resolved. `draft` is a request left behind by a quote-to-order flow that never reached the merchant."
          },
          "expiration_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Always present. Accept before this instant; null means the merchant has not set one yet (the quote may still be expired by a background job)."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 code; all amounts in this document are in this currency"
          },
          "totals": {
            "type": "object",
            "properties": {
              "gross": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total including tax"
              },
              "net": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total excluding tax"
              },
              "tax_status": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "gross",
                  "net",
                  "tax-free",
                  null
                ],
                "description": "Which total is the customer-facing authoritative amount; null if the commercial backend did not report one"
              }
            }
          },
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "product_id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                },
                "unit_price": {
                  "type": "number",
                  "description": "Current (offered) price per unit"
                },
                "total_price": {
                  "type": "number"
                },
                "requested_unit_price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Buyer's last ask per unit, if any. Always null on a shop that cannot record per-line asks (see Price semantics)."
                }
              }
            }
          },
          "comments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "comment": {
                  "type": "string"
                },
                "author": {
                  "type": "string",
                  "enum": [
                    "buyer",
                    "merchant"
                  ]
                },
                "created_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "order": {
            "type": "object",
            "description": "Present only after acceptance",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "order_number": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "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."
          },
          "ucp": {
            "type": "object",
            "description": "Protocol envelope added to every successful response; `capabilities` appears once an agent has negotiated.",
            "properties": {
              "version": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "const": "success"
              },
              "capabilities": {
                "type": "object"
              }
            }
          }
        }
      },
      "AcceptResponse": {
        "description": "The accepted quote, with the order block populated.",
        "$ref": "#/components/schemas/Quote"
      },
      "Error": {
        "type": "object",
        "required": [
          "ucp",
          "messages"
        ],
        "properties": {
          "ucp": {
            "type": "object",
            "description": "Protocol envelope. `status` is `error`; the failure itself is in `messages`.",
            "properties": {
              "version": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "const": "error"
              }
            }
          },
          "messages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "description": "Machine-readable error code",
                  "examples": [
                    "validation_error",
                    "resource_not_found",
                    "unsupported_capability"
                  ]
                },
                "content": {
                  "type": "string"
                },
                "severity": {
                  "type": "string"
                }
              }
            }
          }
        },
        "description": "Standard UCP error envelope produced by the SDK (validation, not-found, idempotency-conflict and unsupported-capability errors). There is no top-level `status`: the status lives in `ucp`."
      },
      "QuoteList": {
        "type": "object",
        "required": [
          "quotes",
          "total",
          "limit",
          "page"
        ],
        "properties": {
          "quotes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Quote"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total quotes for this customer, not just this page"
          },
          "limit": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          }
        }
      }
    }
  }
}
