# PATCH /api/v2/sales-invoices/{sales_invoice}

Update a sales invoice

- MCP id: sales-invoices.update
- Scope: api:write:sales_invoices
- Permission: sales_invoices::update
- Needs the billing feature on your organisation
- Rate cost: 1 unit, write bucket. A write costs the same whatever `view` you ask for.
- Cache-Control: private, no-store

Partially updates the invoice header: absent keys are untouched, and the line rows are edited through their own endpoints.

Send `If-Match` with the `ETag` from your last read to detect a concurrent edit, which answers `412`. A legacy invoice cannot be changed at all and answers `409` `state-conflict` with reason `locked_by_legacy`. An invoice that has already synced to the accounting system can still be edited here, which goes further than plnnrly: plnnrly closes its edit form once an invoice is synced, and the change will not be pushed to the accounting system again. status is never writable: it is stripped from the payload, and any update by a caller without `sales_invoices::finalize` additionally returns the invoice to `pending_approval`, so an already-approved invoice cannot be changed and synced without a finalizer re-checking it.

## Parameters
- sales_invoice (integer or string, path): The SalesInvoice's numeric id. Or its invoice_number, matching `[A-Z]+-[0-9]{2}-[0-9]{5}|L-[0-9]+`. Accepts a soft-deleted record.
- view (string, optional): Which field set to render.
- trashed (string, optional): Whether soft-deleted rows are included.
- X-Request-Id (header, optional): A correlation id of your own. It is recorded in our logs next to the server's request id but never returned: the response header always carries the server's id. A value that does not match the pattern is discarded silently, never rejected.
- If-Match (header, optional): The ETag a write is conditional on. A stale validator is 412 precondition-failed.

## Request body
- customer_id (integer → Customer.id, optional): Customer this invoice is billed to.
- project_id (integer or null → Project.id, optional): Project this invoice was raised for, when it was raised from one.
- customer_subscription_id (integer or null → CustomerSubscription.id, optional): Customer subscription this invoice was generated from, when it was generated from a renewal.
- invoice_date (date (YYYY-MM-DD), optional): Invoice date, a zone-free calendar date. Newest first is the default sort.
- due_date (date (YYYY-MM-DD) or null, optional): Date payment is due, a zone-free calendar date. Null when no term was applied. Must be on or after invoice_date.
- reference (string or null ≤ 255, optional): The customer's own reference, typically a purchase-order number, reproduced on the invoice.
- description (string or null ≤ 255, optional): One-line description of what was invoiced.
- internal_notes (string or null, optional): Notes for colleagues. Never rendered on the invoice the customer receives.
- should_email (boolean, optional): Whether the invoice is emailed to the customer when it syncs. The email is sent through the accounting integration, so it is forced to `false` in an organisation with no integration configured. Writing it requires `sales_invoices::finalize`.

Unknown keys are refused with `422` validation-failed, naming the key.

## Response 200
- id (integer, required): Unique identifier.
- customer_id (integer, required): Customer this invoice is billed to.
- customer (Ref or CustomerSummary, required): The customer this invoice is billed to. Expanding it with `?include=customer` additionally requires the `customers::read` permission and a token scope covering the customers prefix.
- project_id (integer or null, required): Project this invoice was raised for, when it was raised from one.
- customer_subscription_id (integer or null, required): Customer subscription this invoice was generated from, when it was generated from a renewal.
- invoice_number (string ≤ 255, required, server-assigned): Invoice number, allocated by the server from the organisation's yearly sequence: B-26-00001 for an invoice raised in this flow, L-1234 for one converted from a legacy invoice. It also works in the path: `GET sales-invoices/B-26-00001` works. This is the field `?q=` searches.
- reference (string or null ≤ 255, required): The customer's own reference, typically a purchase-order number, reproduced on the invoice.
- description (string or null ≤ 255, required): One-line description of what was invoiced.
- invoice_date (date (YYYY-MM-DD), required): Invoice date, a zone-free calendar date. Newest first is the default sort.
- due_date (date (YYYY-MM-DD) or null, required): Date payment is due, a zone-free calendar date. Null when no term was applied.
- status (string, required, server-assigned): Lifecycle status. Resolved by the server on create and never writable: a caller without `sales_invoices::finalize` creates a `pending_approval` invoice, and any update by such a caller returns the invoice to `pending_approval`.
- should_email (boolean, required): Whether the invoice is emailed to the customer when it syncs. The email is sent through the accounting integration, so it is forced to `false` in an organisation with no integration configured. Writing it requires `sales_invoices::finalize`.
- subtotal (decimal (2 dp), required, server-assigned): Sum of the row line totals excluding VAT, as a decimal string. Recomputed from the rows; never written directly.
- vat_total (decimal (2 dp), required, server-assigned): Sum of the row VAT amounts, as a decimal string. Recomputed from the rows; never written directly.
- total (decimal (2 dp), required, server-assigned): Subtotal plus VAT, as a decimal string. Recomputed from the rows; never written directly.
- exact_invoice_id (string or null ≤ 255, required, server-assigned): The accounting system's own identifier for this invoice, written by the sync. Not a local id.
- exact_journal (string or null ≤ 255, required, server-assigned): Sales journal the invoice is booked in, resolved from the organisation's accounting settings on create. Null in an organisation with no integration configured.
- sync_status (string, required, server-assigned): How far the invoice has got through the accounting sync. Set on create: local in an organisation with no integration configured, pending otherwise.
- sync_error (string or null, required, detail only, server-assigned, multiline): Why the last sync attempt failed, verbatim from the accounting system. Null while nothing has failed.
- synced_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the invoice last synced successfully.
- sync_attempted_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the invoice was last handed to the sync, successfully or not.
- paid_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the invoice was settled. Set on create for a local invoice that is immediately definitive; otherwise written when payment is registered.
- is_legacy (boolean, required, server-assigned): Whether this invoice was converted from a legacy invoice. Such an invoice is read-only: PATCH and DELETE answer `409` `state-conflict` with reason `locked_by_legacy`.
- legacy_invoice_id (integer or null, required, server-assigned): The id of the legacy invoice this invoice was converted from, so it can be matched with `GET /api/v2/invoices`. The legacy invoice may since have been removed.
- internal_notes (string or null, required, detail only, multiline): Notes for colleagues. Never rendered on the invoice the customer receives.
- rows_count (integer ≥ 0, required): Number of line rows on this invoice.
- created_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Creation timestamp.
- updated_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Last-write timestamp; use for incremental sync. It also feeds the `ETag` revision, because a sales invoice has no `version` field.
- deleted_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): Soft-delete timestamp, `null` while live.
- links (SalesInvoiceLinks, required)
  - self (url, required): The canonical URI of this record.
  - rows (url, required): Line rows on this invoice.

## Errors
- 403 forbidden-by-policy | insufficient-permission | insufficient-token-scope: Forbidden.
- 409 state-conflict: Conflict.
- 412 precondition-failed: Precondition failed.
- 415 unsupported-media-type: Unsupported media type.
- 422 unprocessable-relationship | validation-failed: Unprocessable content.
- 400 idempotency-key-required | invalid-cursor | invalid-query-parameter | malformed-request | unsupported-parameter: Bad request.
- 401 token-expired | unauthenticated: Unauthenticated.
- 404 endpoint-not-found | resource-not-found: Not found.
- 406 not-acceptable: Not acceptable.
- 429 rate-limit-exceeded: Too many requests.
- 500 internal-error: Server error.
- 503 tenant-unavailable: Service unavailable.

## Example
```bash
curl -X PATCH \
  'https://<your-url>/api/v2/sales-invoices/1' \
  -H 'Authorization: Bearer <token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: *' \
  -d '{
    "due_date": "2026-03-31"
  }'
```
