---
title: "Update a worksheet"
method: PATCH
path: "/v1/worksheets/{id}"
tags: ["Worksheets"]
---

# Update a worksheet

`PATCH /v1/worksheets/{id}`

## Request

- Base URL: `https://api.officina.it`
- URL: `https://api.officina.it/v1/worksheets/{id}`
- Auth: HTTP bearer

Partial update following the standard three-state PATCH
semantics (omitted = unchanged, `null` = clear, value = set).

Only top-level editable fields are exposed here. Use
`POST /v1/worksheets/{id}/transitions` to change `status`.
Line-item mutation is out of scope for v1 — a dedicated
sub-resource will land in a later pass.

Fields intentionally NOT patchable: `id`, `company_id`,
`created_by`, `seq_number`, `status`, `customer_id`,
`vehicle_id`, all totals, all `*_at` timestamps,
`line_items`, `documents`.

Foreign-key references in the body (`work_category_id`,
`assigned_to`) must resolve to resources owned by the
authenticated user's company. Unreachable refs return 422
`parameter_invalid` pointing at the offending field.

**Frozen worksheets** — when the worksheet's linked invoice
has been issued, the worksheet is frozen: its totals are
now part of the invoice's accounting record and must not
desync. PATCH returns 409 `conflict`. Cancel the invoice
with a credit note before editing.

## Request body

- WorksheetPatchRequest: Body of `PATCH /v1/worksheets/{id}`. Every field carries three-state semantics: - Field omitted → leave the entity field untouched. - Field present with value `null` → clear the entity field. - Field present with a value → set the entity field. Fields not listed here are intentionally NOT patchable: `id`, `company_id`, `created_by`, `seq_number`, `status` (use the transitions endpoint), `customer_id`, `vehicle_id`, all totals, all `*_at` timestamps, `line_items`, `documents`.
  - `title` string, nullable
  - `comments` string, nullable
  - `internal_comments` string, nullable
  - `assigned_to` string, nullable
  - `work_category_id` string, nullable: Single-category form for clients that handle one category, accepted in every revision with the same meaning: a value replaces the first category of the list and keeps the others; `null` removes the first one. To set or clear the full list, send `work_category_ids` instead (not both).
  - `work_category_ids` string[], nullable: Replaces the document's whole category list, in the order given. Omitted → untouched; `[]` or `null` → removes every category; a list → becomes the new list. Same validation as on create (max 10, de-duplicated, company's categories only). Do not send together with `work_category_id`: both → `422 parameter_invalid`. Available since revision 2026-10-03.
  - `km` number, nullable
  - `total_work_cost` number, nullable: Sets the internal labour cost by hand (0 included); line changes no longer move it. `null` goes back to automatic (the lines' sum). Omitted → untouched. Negative → `422`.
  - `total_article_cost` number, nullable: Same as `total_work_cost`, for the parts cost.

## Response `200`

Worksheet updated. Returns the full post-update entity.

- Worksheet: A workshop work order. Belongs to one customer and (usually) one vehicle. Lifecycle: draft → waiting → started → completed → delivered → archived. Each state has a matching nullable `*_at` timestamp populated by the server when the transition fires. Totals are server-computed from the embedded line items and rounded to the working currency precision. They are read-only on the wire. `line_items` is populated on detail / write responses and omitted (or `null`) on list responses to keep page payloads small.
  - `object` 'worksheet', required
  - `id` string, required
  - `seq_number` integer, required: Per-company human-friendly sequence number. Allocated server-side at creation; never reused.
  - `status` 'draft' | 'waiting' | 'started' | 'completed' | 'delivered' | 'archived', required
  - `title` string, nullable
  - `comments` string, nullable
  - `internal_comments` string, nullable: Workshop-internal notes; surfaced through the mobile app for the workshop's own users (no customer-facing channel exposes this field).
  - `customer_id` string, required
  - `vehicle_id` string, nullable
  - `assigned_to` string, nullable
  - `work_category_id` string, nullable
  - `km` number, nullable: Odometer reading captured when the worksheet was opened. Free-form decimal because some workshops record fuel mileage or hour-meter values in the same field.
  - `created_by` string, required
  - `created_by_full_name` string: Display string of the user who created the worksheet — denormalized for list rendering without a join.
  - `customer_name` string
  - `vehicle_name` string, nullable
  - `vehicle_license` 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
  - `total_before_tax` number
  - `total_tax` number
  - `total_after_tax` number
  - `total_after_discount` number
  - `total_discount` number
  - `total_price` number
  - `total_payments` number
  - `outstanding_payments` number
  - `fiscal_document_type` string: Which fiscal document the worksheet produces: "fattura" (default) or "scontrino".
  - `invoice_id` string, nullable: Detail responses only — the first linked invoice, when one exists. Drives the Vedi Fattura action.
  - `receipt_id` string, nullable: Detail responses only — the linked scontrino for scontrino-type worksheets, when one exists.
  - `linked_invoices` WorksheetFiscalDocument[]: Detail responses only — compact rows for the Fattura tab (fattura-type worksheets).
    - `object` 'worksheet_fiscal_document', required
    - `id` string, required
    - `seq_number` integer, required
    - `issued_at` string, date-time, nullable, required
    - `created_at` string, date-time, required
    - `total_after_discount` number, double, required
    - `outstanding_payments` number, double, required
  - `linked_receipt` WorksheetFiscalDocument: One compact linked fiscal document on a worksheet detail — seq_number 0 means a draft ("Non emessa" / "Bozza").
    - `object` 'worksheet_fiscal_document', required
    - `id` string, required
    - `seq_number` integer, required
    - `issued_at` string, date-time, nullable, required
    - `created_at` string, date-time, required
    - `total_after_discount` number, double, required
    - `outstanding_payments` number, double, required
  - `frozen` boolean: Detail responses only (since 2026-07-19) — true when a linked invoice has a non-draft electronic invoice; mutations are rejected with 409 and the app hides/blocks Modifica.
  - `total_work_cost` number: Internal labour cost ex-VAT (Costi di lavorazione): the value typed by hand when `work_cost_manual` is true, otherwise the sum of the lines' `unit_cost × quantity` for unit types h, gg, lav, min. Correct on every revision and on list responses since 2026-10-15 (lists used to return 0).
  - `total_article_cost` number: Internal parts cost ex-VAT: the value typed by hand when `article_cost_manual` is true, otherwise the sum of the other lines' `unit_cost × quantity`.
  - `work_cost_manual` boolean: True when `total_work_cost` was typed by hand; line changes no longer move it. Reset with `PATCH` `total_work_cost: null`.
  - `article_cost_manual` boolean: True when `total_article_cost` was typed by hand.
  - `waiting_at` string, date-time, nullable
  - `started_at` string, date-time, nullable
  - `completed_at` string, date-time, nullable
  - `delivered_at` string, date-time, nullable
  - `archived_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `deleted_at` string, date-time, nullable: Soft-deletion timestamp. Populated on the response from DELETE /v1/worksheets/{id}. Worksheets with a non-null deleted_at are not returned by list/detail endpoints.
  - `line_items` WorkLineItem[], nullable: Embedded line items. Populated on detail / create / patch / delete / transition responses. List responses set this to `null` to keep the payload small.
    - `object` 'work_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: Optional link to a catalog article. When set, edits flow through the article inventory bookkeeping.
    - `description` string, required
    - `quantity` number, required
    - `unit_type` string, required
    - `unit_price` number, required
    - `unit_discount_rate` number
    - `tax_rate` number, required
    - `vat_code` string, nullable
    - `total_price` number, required
    - `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. `null` when unknown.
    - `total_before_tax` number
    - `total_tax` number
    - `total_after_tax` number
    - `position` integer, required
    - `created_at` string, date-time, required
  - `documents` WorksheetDocument[], nullable: Attachments (Documenti Allegati). Populated on the same responses as `line_items`; null on lists.
    - `object` 'worksheet_document', required
    - `id` string, required
    - `name` string, required
    - `content_type` string, required
    - `description` string, nullable
    - `url` string, required
    - `file_size_bytes` integer, required
    - `created_at` string, date-time, required

## 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.
- `409`: The request conflicts with the current state of the resource. Examples: issuing an invoice that has already been issued; back-dating an issuance that would violate the monotonic per-company sequence; deleting a credit note whose SDI electronic counterpart has already been transmitted and cannot be retracted; mutating a worksheet whose linked invoice has been issued (the worksheet is frozen — its totals are now part of the issued invoice's accounting record and must not desync).
- `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)
