---
title: "Update a quote item"
method: PATCH
path: "/quotes/{id}/items/{itemId}"
tags: ["Quotes"]
---

# Update a quote item

`PATCH /quotes/{id}/items/{itemId}`

## Request

- Base URL: `https://joinways.app/api/v1`
- URL: `https://joinways.app/api/v1/quotes/{id}/items/{itemId}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://joinways.app/oauth/authorize`, token URL `https://joinways.app/api/oauth/token`, scopes: `mcp.write`)

Updates a line of this quote; `total` is recomputed when a pricing field changes.

**Package line** (`is_package: true`): `quantity`, `unit_price` (the price of one package unit), `is_option`, `is_offered`, `discount_percent`, the day and the order are carried over to its articles, and the response is the package as it now stands. `vat` is ignored (each article keeps its rate). `discount_amount` above 0 and `is_discount` are refused (`package_discount_amount`). The id of an article (`package.articles[].id`) is refused (`package_article`): update the package line. **Role permission:** `quotes.manage`.

## Headers

- `Idempotency-Key` string

## Request body

- QuoteItemUpdate: Every field is optional; `total` is recomputed when a pricing field changes. On a package line, `unit_price` is the price of one package unit, `vat` is ignored, `discount_amount` above 0 and `is_discount` are refused.
  - `title` string
  - `description` string
  - `quantity` number
  - `unit_price` number
  - `vat` number
  - `location_item_id` string, uuid, nullable
  - `is_option` boolean
  - `is_discount` boolean
  - `is_offered` boolean
  - `discount_amount` number
  - `discount_percent` number, nullable
  - `event_day` integer
  - `event_day_date` string, date, nullable
  - `sort_order` integer

## Response `200`

The updated item.

- QuoteItem: A line item of a quote. A package (`is_package: true`) is ONE item priced as a whole, with its detail under `package`.
  - `id` string, uuid
  - `quote_id` string, uuid
  - `account_id` string, uuid, nullable
  - `title` string
  - `description` string
  - `quantity` number
  - `unit_price` number: Price per unit, excluding tax. On a package: the price of one package unit (`package.unit_price`).
  - `vat` number, nullable: Tax rate as a percentage (e.g. 20 for 20 %). A package mixes rates: this is only the lowest of them, read `package.tax_rates` and never compute its tax from `vat`.
  - `total` number, nullable: Computed line total, excluding tax, after the line discount. On a package: the sum of its articles (`package.total_excl_tax`).
  - `is_package` boolean: The line is a package; `package` is then present.
  - `parent_item_id` string, uuid, nullable: The package line an article belongs to. `null` on the items returned: the articles of a package are under its `package.articles`, not items of their own.
  - `package` QuoteItemPackage: Only on a package line. Amounts are summed article by article, like the quote totals, so they can differ by a cent from `unit_price × quantity`.
    - `unit_price` number, required: Price of one package unit, excluding tax, before discount.
    - `total_excl_tax` number, required: Sum of the articles, after the package's discount.
    - `total_tax` number, required
    - `total_incl_tax` number, required
    - `tax_rates` number[], required: Distinct tax rates of the articles, ascending, in %.
    - `articles` QuoteItemPackageArticle[], required: What the package is made of, in the order of its composition. Read-only: written by Joinways from the package line. Their ids are refused by every write (`package_article`).
      - `id` string, uuid
      - `location_item_id` string, uuid, nullable: Catalog item of the article.
      - `title` string
      - `description` string, nullable
      - `quantity` number: Package quantity × `package_unit_quantity`.
      - `package_unit_quantity` number: Quantity of the article in one package unit.
      - `unit_price` number: Unit price of the article inside the package, excluding tax.
      - `vat` number: Tax rate of the article, in %.
      - `total` number: Amount of the article, excluding tax, after the package's discount.
  - `location_item_id` string, uuid, nullable: The catalog item this line was priced from, if any.
  - `min_price_applied` number, nullable: Catalog minimum price the line was checked against.
  - `is_option` boolean, nullable
  - `is_discount` boolean, nullable
  - `is_offered` boolean, nullable: Offered (free) line; bypasses the catalog minimum-price check.
  - `discount_amount` number, nullable
  - `discount_percent` number, nullable
  - `event_day` integer: 1-based day this line belongs to (multi-day events).
  - `event_day_date` string, date, nullable
  - `sort_order` integer: Display order within the day.
  - `client_selected_at` string, date-time, nullable: When the client ticked this option on the public quote.
  - `inherited_from_item_id` string, uuid, nullable
  - `ai_rationale` string, nullable
  - `created_at` string, date-time

## Other responses

- `400`: Validation failed (`invalid_request`). For a body that fails the schema, `details` holds `formErrors` and `fieldErrors`.
- `401`: Missing, invalid, expired or revoked bearer token (`unauthorized`).
- `403`: `forbidden`: the token lacks the scope, its owner's role lacks the permission named by the operation, or the workspace has neither the Pro plan nor a trial (`details.reason` `plan_not_eligible`).
- `404`: Not found in this workspace, or the id of the path is not a UUID (`not_found`).
- `409`: The quote's state refuses the operation (`conflict`); retrying the same call fails the same way until that state changes (except `amendment_in_flight`, transient). `error.details.reason` (see `QuoteErrorReason`): `quote_locked` (signed, invoiced or paid: amend or cancel instead), `quote_signed`, `quote_cancelled`, `quote_not_cancellable`, `quote_has_active_invoice` (credit the invoice first), `quote_has_amendments` (a quote with a live amendment is frozen), `quote_superseded` (a replaced version is history), `amendment_in_flight` (one amendment at a time), `amendment_nested`, `parent_not_active`, `parent_not_signed`, `parent_fully_invoiced`, `quote_changed` (the status changed meanwhile), `invalid_status`, `not_a_platform_version`, `event_closed` (event lost or cancelled), `event_archived`, `platform_quote_read_only` (a platform quote, `source: platform`, is the platform's document: amounts, discounts, currency and lines are read-only, its status only takes `accepted` or `rejected`), `platform_quote_delete_forbidden` (cancel or refuse it instead), `platform_version_signed` (another version is signed), `quote_conflict` (another live contract must be cancelled or refused first).
- `429`: Too many requests (`rate_limited`). `details.reason`: `per-ip`, `per-account-minute`, `per-account-day`, or `email_rate_limited` (email budget). Always carries `Retry-After`: wait that many seconds.

---

[API](https://skmtc.dev/joinways/apis/joinways-public-api.md) · [All operations](https://skmtc.dev/joinways/apis/joinways-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/joinways/apis/joinways-public-api/revisions/39e84309953e?raw)
