---
title: "Update an invoice"
method: PATCH
path: "/v1/invoices/{id}"
tags: ["Invoices"]
---

# Update an invoice

`PATCH /v1/invoices/{id}`

## Request

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

Partial update following the design philosophy:

- Omitted JSON keys leave the corresponding entity fields
  untouched.
- Explicit JSON `null` clears the field (SQL NULL) where the
  column is nullable.
- Concrete values overwrite the entity field.

Non-nullable columns (`payment_method`, `payment_condition`,
`payment_due_at`, `vat_exigibility`) reject explicit `null`
with 422 `parameter_invalid`.

Fields NOT patchable through this endpoint: `id`,
`seq_number`, `status`, `customer_id`, `vehicle_id`,
`worksheet_id`, all totals, all timestamps except
`payment_due_at`. Line items are not mutable via PATCH.
Use POST `/v1/invoices/{id}/issue` to transition a draft.

## Request body

- InvoicePatchRequest: Body of `PATCH /v1/invoices/{id}`. Three-state per field: omitted (leave alone), `null` (clear, where nullable), concrete value (set). Non-nullable columns reject `null` with 422 `parameter_invalid`. Fields not listed here are intentionally NOT patchable: `id`, `seq_number`, `status`, `customer_id`, `vehicle_id`, `worksheet_id`, all totals, all timestamps except `payment_due_at`. Line item add/remove is not exposed. `title`, `comments`, `internal_comments` are deliberately absent today: the service-layer Update contract doesn't carry these columns yet, so accepting them on the wire would silently drop the write. They will be added once the service is extended.
  - `payment_method` string
  - `payment_condition` string
  - `payment_due_at` string, date-time: RFC 3339 timestamp supplied by the client. Must fall between 1900-01-01 and 2100-12-31. A year outside that window is a typo (2025 mistyped as 0025 is the common one), and the app's own date pickers refuse the same range — a record written with one would not be editable in the web UI afterwards. Out of range → 422 `parameter_invalid` with `param` pointing at the offending field.
  - `vat_exigibility` string
  - `sezionale_id` string, nullable
  - `pa_document_type` string, nullable
  - `pa_document_number` string, nullable
  - `pa_document_date` string, date-time, nullable: Nullable client-supplied RFC 3339 timestamp. Same 1900-01-01 … 2100-12-31 window as `WritableTimestamp`; `null` clears the field.
  - `pa_cig` string, nullable
  - `pa_cup` string, nullable

## Response `200`

Invoice updated.

- Invoice: A workshop invoice. Multi-tenant: belongs to one company. Created from a worksheet; transitions draft → issued via `POST /v1/invoices/{id}/issue`.
  - `object` 'invoice', required
  - `id` string, required
  - `status` 'draft' | 'issued', required: Derived from `issued_at`. Soft-deleted invoices 404 before reaching the client — "deleted" is never emitted.
  - `title` string, nullable
  - `comments` string, nullable
  - `internal_comments` string, nullable
  - `payment_method` string, required
  - `payment_condition` string, required
  - `payment_due_at` string, date-time, required
  - `vat_exigibility` string, required
  - `sezionale_id` string, nullable
  - `pa_document_type` string, nullable
  - `pa_document_number` string, nullable
  - `pa_document_date` string, date-time, nullable
  - `pa_cig` string, nullable
  - `pa_cup` string, nullable
  - `customer_id` string, required
  - `vehicle_id` string, nullable
  - `worksheet_id` string, nullable
  - `seq_number` integer, required: Per-company monotonic invoice number. 0 on drafts; assigned atomically at issuance.
  - `total_before_tax` number, double, required: Money. Float64 — clients render via the workshop locale formatter; avoid arithmetic.
  - `total_tax` number, double, required
  - `total_after_tax` number, double, required
  - `total_discount` number, double, required
  - `total_after_discount` number, double, required
  - `total_payments` number, double, required
  - `outstanding_payments` number, double, required
  - `customer_name` string, required
  - `vehicle_name` string, nullable
  - `vehicle_license` string, nullable
  - `created_by_full_name` string, required
  - `sezionale_code` string, nullable
  - `work_category_name` string, nullable
  - `work_category_names` string[], required: Names of the linked worksheet's work categories, in order. `[]` when the invoice has no worksheet or the worksheet has none. `work_category_name` is the first one. Available since revision 2026-10-03.
  - `sdi_status` string, required: Italian SDI e-invoice lifecycle status. Empty string when no e-invoice exists for this invoice. Possible non-empty values: "sent", "rejected", "pending_normal", "pending_warning", "pending_late".
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `issued_at` string, date-time, nullable: Populated once the invoice is issued.
  - `deleted_at` string, date-time, nullable: Soft-deletion timestamp. Populated only on the response from DELETE /v1/invoices/{id}.
  - `line_items` InvoiceLineItem[]: Populated on detail and post-mutation responses. Omitted from list rows to keep payloads small.
    - `object` 'invoice_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
    - `quantity` number, double, required
    - `unit_type` string, required
    - `unit_price` number, double, required
    - `unit_cost` number, double, nullable
    - `unit_discount_rate` number, double, required
    - `tax_rate` number, double, required
    - `vat_code` string, nullable
    - `vat_description` string, nullable
    - `total_price` number, double, required
    - `total_before_tax` number, double, required
    - `total_tax` number, double, required
    - `total_after_tax` number, double, 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.
- `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)
