---
title: "Mutate quote line items in a batch"
method: POST
path: "/v1/quotes/{id}/line-items/batch"
tags: ["Quotes"]
---

# Mutate quote line items in a batch

`POST /v1/quotes/{id}/line-items/batch`

## Request

- Base URL: `https://api.officina.it`
- URL: `https://api.officina.it/v1/quotes/{id}/line-items/batch`
- Auth: HTTP bearer

Adds, updates, removes, and reorders the quote's line items in a
single atomic diff, then returns the full quote with recomputed
totals and the updated `line_items` array. The top-level quote
PATCH is metadata-only; all line-item mutation flows through this
endpoint. Identical wire shape to the worksheet batch endpoint.

**Operations** (all arrays optional; empty body is a no-op):
`add`, `update` (full replace by id), `remove` (by id),
`reorder`. An id may appear in at most one of
`remove`/`update`/`reorder`. As with worksheets, `update` is a
remove+add internally so an updated line receives a NEW id;
`tax_rate` is resolved server-side from `vat_code`.

Quotes are estimates — there is no invoice-freeze rule and
article references on lines do not move inventory stock (they
are validated for ownership only).

## Request body

- LineItemsBatchRequest: Body of `POST /v1/worksheets/{id}/line-items/batch` and `POST /v1/quotes/{id}/line-items/batch`. Applies a single diff against the resource's current line-item set; totals are recomputed server-side from the result and the full resource is returned. All four arrays are optional; an empty body is a no-op. An id may appear in at most one of `remove`/`update`/`reorder` — listing it twice is a 422. Ids in `remove`/`update`/`reorder` must belong to the target resource.
  - `add` LineItemInput[]
    - `line_type` 'item' | 'note' | 'separator': Optional, default `item`. A `note` needs only `description`; a `separator` needs nothing. For both, the numeric fields, `vat_code` and `article_id` are ignored and stored as zero. On `update`, leaving it out keeps the line's current type.
    - `description` string: Required for `item` and `note`, non-empty.
    - `quantity` number: Required for `item`, > 0.
    - `unit_type` string: Required for `item` (e.g. "pz", "kg", "L", "kit").
    - `unit_price` number: Required for `item`, >= 0.
    - `unit_discount_rate` number: Percent, 0..100. Defaults to 0.
    - `vat_code` string: Required for `item`; must exist in the company VAT table.
    - `article_id` string, nullable: Optional link to a catalog article. When set it must reference an article owned by the caller's company. On worksheets, article-backed lines move inventory stock.
    - `unit_cost` number, nullable: Optional internal cost per unit, ex-VAT, for the worksheet's internal costs. Omitted: an article line takes the article's purchase price at that moment, a line without article has no cost; an `update` that keeps the same article keeps the line's current cost. May be negative (a credit, like a negative purchase price); beyond ±99.999.999,99 → `422` on `/unit_cost`.
    - `position` integer, nullable: Optional ordering position. When omitted the item is appended after the current maximum position.
  - `update` LineItemUpdate[]
    - `line_type` 'item' | 'note' | 'separator': Optional, default `item`. A `note` needs only `description`; a `separator` needs nothing. For both, the numeric fields, `vat_code` and `article_id` are ignored and stored as zero. On `update`, leaving it out keeps the line's current type.
    - `description` string: Required for `item` and `note`, non-empty.
    - `quantity` number: Required for `item`, > 0.
    - `unit_type` string: Required for `item` (e.g. "pz", "kg", "L", "kit").
    - `unit_price` number: Required for `item`, >= 0.
    - `unit_discount_rate` number: Percent, 0..100. Defaults to 0.
    - `vat_code` string: Required for `item`; must exist in the company VAT table.
    - `article_id` string, nullable: Optional link to a catalog article. When set it must reference an article owned by the caller's company. On worksheets, article-backed lines move inventory stock.
    - `unit_cost` number, nullable: Optional internal cost per unit, ex-VAT, for the worksheet's internal costs. Omitted: an article line takes the article's purchase price at that moment, a line without article has no cost; an `update` that keeps the same article keeps the line's current cost. May be negative (a credit, like a negative purchase price); beyond ±99.999.999,99 → `422` on `/unit_cost`.
    - `position` integer, nullable: Optional ordering position. When omitted the item is appended after the current maximum position.
    - `id` string, required
  - `remove` string[]: Ids of line items to delete.
  - `reorder` LineItemReorder[]
    - `id` string, required
    - `position` integer, required

## Response `200`

Batch applied. Returns the updated quote (with `line_items`).

- Quote: A customer-facing job estimate. Lifecycle: `waiting` → `accepted` / `rejected`. Totals are server-computed from the embedded line items and are read-only on the wire.
  - `object` 'quote', required
  - `id` string, required
  - `seq_number` integer, required
  - `status` 'waiting' | 'accepted' | 'rejected', required
  - `title` string, nullable
  - `comments` string, nullable
  - `internal_comments` string, nullable
  - `customer_id` string, required
  - `customer_name` string, required
  - `vehicle_id` string, nullable
  - `vehicle_name` string, nullable
  - `vehicle_license` string, nullable
  - `work_category_id` string, nullable
  - `work_category_name` string, nullable
  - `work_category_ids` string[], required: Every work category ("categoria") of the document, in the order chosen. `work_category_id` (kept for clients that handle a single category) always equals the first. `[]` when the document has none, never null. Available since revision 2026-10-03.
  - `work_categories` WorkCategory[], required: The same list as `work_category_ids` with each category's name, same order. `work_category_name` is the first one's name. Available since revision 2026-10-03.
    - `object` 'work_category', required
    - `id` string, uuid, required
    - `name` string, required
  - `km` number, double, nullable
  - `worksheet_id` string, nullable
  - `worksheet_seq_number` integer, nullable
  - `worksheet_created_at` string, date-time, nullable
  - `total_before_tax` number, double
  - `total_tax` number, double
  - `total_after_tax` number, double
  - `total_discount` number, double
  - `total_discount_before_tax` number, double
  - `total_after_discount` number, double
  - `total_work` number, double
  - `total_articles` number, double
  - `hide_discounts` boolean
  - `line_items` QuoteLineItem[], nullable: Embedded line items. Populated on detail / write responses; list responses set this to `null` to keep payloads small.
    - `object` 'quote_line_item', required
    - `id` string, required
    - `line_type` 'item' | 'note' | 'separator', required: `item` is a priced line. `note` is a description-only line and `separator` an empty spacer row: both print without quantity, price and VAT and carry zero amounts (ignore their numeric fields). Lines from documents without types report `item`.
    - `article_id` string, nullable
    - `description` string, required
    - `unit_type` string, required
    - `quantity` number, double, required
    - `unit_price` number, double, required
    - `unit_discount_rate` number, double
    - `tax_rate` number, double
    - `vat_code` string, nullable
    - `total_price` number, double
    - `unit_cost` number, nullable: Internal cost per unit, ex-VAT, never printed on documents. Fixed when the line was written (the article's purchase price at that moment, or the `unit_cost` sent); converting the quote carries it to the worksheet's line. `null` when unknown.
    - `total_before_tax` number, double
    - `total_tax` number, double
    - `total_after_tax` number, double
    - `position` integer, required
    - `created_at` string, date-time, required
  - `delivered_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `archived_at` string, date-time, nullable
  - `deleted_at` string, date-time, nullable

## Other responses

- `400`: Request body is unparseable or X-API-Version is missing.
- `401`: Missing, malformed, expired, or wrong-audience bearer token.
- `402`: The company has no active subscription, or its plan does not include API access. API access is included in the Business and Illimitato plans.
- `403`: The authenticated user lacks the required permission.
- `404`: Resource does not exist OR belongs to a different company. The two cases are intentionally indistinguishable.
- `415`: Content-Type is not `application/json`, or Accept rejects JSON.
- `422`: A request field is missing or invalid (fast-fail).
- `500`: Unhandled server error. Detail is logged, not exposed.

---

[API](https://skmtc.dev/officina/apis/officina-it-api.md) · [All operations](https://skmtc.dev/officina/apis/officina-it-api/llms.txt) · [OpenAPI document](https://skmtc.dev/officina/apis/officina-it-api/revisions/7da3fccfe551?raw)
