Skip to main content

Quotes

Use quote operations when a merchant integration needs to read the sales quotations a merchant has prepared — to mirror them into a CRM, report on the sales pipeline, or follow a quote through to the order it becomes.

A quote can be opened through the API and its positions can be put together and priced there. What still happens in the Pickware administration is everything around the quote: changing the shipping and payment method, the shipping costs, sending the quote to the customer and converting it into an order.

Available operations​

Every line item mutation returns the recalculated Quote, so the new totals come back in the same round trip as the change — select amountTotal, amountNet and lineItems on the result instead of following up with a query.

Example: open a quote for a customer​

createQuote takes the customer and the sales channel and derives everything else: the quote number, the currency and its rounding, the tax handling, and the shipping and payment method the sales channel defaults to.

mutation CreateQuote($input: CreateQuoteInput!) {
createQuote(input: $input) {
id
number
state
taxStatus
amountTotal
}
}
{
"input": {
"customerId": "Q3VzdG9tZXI6MDE4...",
"salesChannelId": "U2FsZXNDaGFubmVsOjAx..."
}
}

Look the customer up with customers. There is no guest equivalent: customerId must name an existing customer account, which createCustomer can produce. The customer's own salesChannelId is usually the sales channel to quote in; salesChannels lists the others.

The result is an empty quote in state draft with a total of zero — add line items and prices before sending it to the customer.

The quote mutations only work on a shop that has quotes enabled. Where they are not, they fail with FORBIDDEN — reading quotes stays available and simply returns an empty connection.

Example: put a product on a quote and depart from the list price​

addQuoteProductLineItem prices the position from the product's price in the quote's sales channel. That is the list price; a quote exists because the offer departs from it, and updateQuoteLineItem is how it does so.

mutation AddPosition($input: AddQuoteProductLineItemInput!) {
addQuoteProductLineItem(input: $input) {
amountTotal
amountNet
lineItems(first: 50) {
edges {
node {
id
productId
quantity
unitPrice
totalPrice
}
}
}
}
}
{
"input": {
"quoteId": "UXVvdGU6...",
"productId": "UHJvZHVjdDow...",
"quantity": 10
}
}

Take the id of the new position from lineItems in the result and set its price:

mutation SetPrice($input: UpdateQuoteLineItemInput!) {
updateQuoteLineItem(input: $input) {
amountTotal
amountNet
}
}
{
"input": {
"lineItemId": "UXVvdGVMaW5lSXRlbTow...",
"unitPrice": 42.5
}
}

Every field on UpdateQuoteLineItemInput except lineItemId is optional and keeps its current value when omitted, so a price-only change sends lineItemId and unitPrice and nothing else.

A quantity of 0 takes the position off the quote, exactly as removeQuoteLineItem would — a position with no units is not a position. A negative quantity is rejected.

Prices on a quote are recalculated, not taken over

Elsewhere in this API a price you send is stored as you sent it. A quote is the exception: unitPrice is a price definition, and the server recalculates the position and the quote totals from it, applying the quote's tax handling and its rounding. So the unitPrice that comes back may differ from the one you sent, and the totals are the server's — read them back from the mutation result rather than computing them client-side.

Example: a free position and a discount​

A position that refers to no catalog product states its own label, price and tax rate:

mutation AddInstallation($input: AddQuoteCustomLineItemInput!) {
addQuoteCustomLineItem(input: $input) {
amountTotal
}
}
{
"input": {
"quoteId": "UXVvdGU6...",
"label": "On-site installation",
"quantity": 2,
"unitPrice": 75.0,
"taxRate": 19
}
}

A discount is a separate kind of position with its own pair of mutations. amount must be positive — it is stored as a negative position and deducted from the total, so a zero or negative amount is rejected rather than reinterpreted. Its tax is derived from the tax buckets of the merchandise already on the quote, so add the merchandise first:

mutation AddDiscount($input: AddQuoteCreditInput!) {
addQuoteCredit(input: $input) {
amountTotal
}
}
{
"input": {
"quoteId": "UXVvdGU6...",
"label": "Project discount",
"amount": 50.0
}
}

updateQuoteLineItem changes product and free positions only and rejects credit and promotion positions. updateQuoteCredit changes credit positions only and rejects everything else, promotions included. removeQuoteLineItem works for every kind of position; a removed promotion stays off the quote.

Example: read a quote with its line items​

query Quote($id: ID!) {
quote(id: $id) {
number
state
validUntil
taxStatus
amountTotal
amountNet
effectiveShippingCosts
lineItems {
edges {
node {
type
label
quantity
unitPrice
totalPrice
}
}
}
}
}

Example: the quotes sent to customers​

sentAt is set the first time a quote goes out and acceptedAt when the customer accepts it. sentAt is never cleared, so a filter on the two timestamps returns the quotes waiting on the customer, but also the declined ones and declined quotes that were reopened as drafts. Select state and keep the quotes in sent to get the ones still waiting for a response.

query SentQuotes {
quotes(filter: { sentAt: { isNull: false }, acceptedAt: { isNull: true } }) {
edges {
node {
id
number
state
sentAt
validUntil
amountTotal
}
}
}
}

Notes and limitations​

  • Only a draft can be changed. Every line item mutation fails once the quote has left the draft state — after it has been sent, accepted or declined. The error names the current state and the states the operation accepts.
  • Each mutation changes one position. There is no batch form, so a quote with ten positions is ten calls; each one locks the quote and recalculates it.
  • taxRate on updateQuoteLineItem only applies to a free position. On a product position the product's own tax rules are kept and taxRate is ignored rather than rejected.
  • label on updateQuoteLineItem applies to every position it accepts. On a product position it replaces the product's name on the quote and on the order created from it, and an empty string "" removes it again, so the position shows the product's name.
  • A negative unitPrice is rejected. A discount is expressed as a credit position, not as a negative price.
  • Quantities are positive integers. addQuoteProductLineItem and addQuoteCustomLineItem reject anything below 1; on updateQuoteLineItem a 0 is the removal shorthand and anything below that is rejected.
  • Sending the quote, converting it into an order, and setting the shipping method, payment method or shipping costs are not exposed yet — those still happen in the Pickware administration.
  • state (draft, sent, accepted, declined) is a derived field and therefore not part of QuoteFilterInput. Filter on sentAt, acceptedAt and orderId instead, or filter client-side.
  • Amounts follow taxStatus (gross, net or tax-free): on a gross quote amountTotal and effectiveShippingCosts include tax, on a net or tax-free quote they do not. amountNet is always the net total, and on a tax-free quote it equals amountTotal.
  • effectiveShippingCosts is what the quote actually charges for shipping. shippingCostsOverrideAmount is only set when a merchant overrode the calculated costs.
  • orderId is set once a quote has been converted, which makes it the join back into Order.
  • The label of a product position is null unless the merchant renamed the position; read the product's name through the product association.
  • The customer, currency, salesChannel, paymentMethod and order associations resolve with the access that reads the quote. The matching root queries (customers, orders, …) each need their own access scope.