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
- List and filter quotes with quotes.
- Load a quote by global ID with quote.
- List quote line items with quoteLineItems, or traverse the
lineItemsfield on Quote. - Load a single line item by global ID with quoteLineItem.
- Open a quote for a customer with createQuote.
- Put a catalog product on a quote with addQuoteProductLineItem, or a free-text position with addQuoteCustomLineItem.
- Change a position's quantity, price and label with updateQuoteLineItem, and take it off the quote with removeQuoteLineItem.
- Grant a discount with addQuoteCredit and change it with updateQuoteCredit.
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.
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
draftstate — 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.
taxRateonupdateQuoteLineItemonly applies to a free position. On a product position the product's own tax rules are kept andtaxRateis ignored rather than rejected.labelonupdateQuoteLineItemapplies 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
unitPriceis rejected. A discount is expressed as a credit position, not as a negative price. - Quantities are positive integers.
addQuoteProductLineItemandaddQuoteCustomLineItemreject anything below1; onupdateQuoteLineItema0is 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 ofQuoteFilterInput. Filter onsentAt,acceptedAtandorderIdinstead, or filter client-side.- Amounts follow
taxStatus(gross,netortax-free): on agrossquoteamountTotalandeffectiveShippingCostsinclude tax, on anetortax-freequote they do not.amountNetis always the net total, and on atax-freequote it equalsamountTotal. effectiveShippingCostsis what the quote actually charges for shipping.shippingCostsOverrideAmountis only set when a merchant overrode the calculated costs.orderIdis set once a quote has been converted, which makes it the join back into Order.- The
labelof a product position isnullunless the merchant renamed the position; read the product's name through theproductassociation. - The
customer,currency,salesChannel,paymentMethodandorderassociations resolve with the access that reads the quote. The matching root queries (customers,orders, …) each need their own access scope.