When to use this endpoint

Use for the legacy job invoice/table editor’s autosave. id is a job ID, not an invoice ID. When a job is linked to a shared invoice, header changes affect that invoice for every linked job.

Read the full invoice and preserve its other rows before replacing rows. This route is not a per-job append or row-level patch API.

Authentication and permissions

Requires signed API-key or CRM session and the authenticated tenant. crm_access bit 64 returns This user does not have access to certain CRM features. Check user permissions.

The helper selects an active invoice but the handler does not independently require an active job when an invoice is already linked. Normal dispatcher user/account/IP restrictions and session CSRF apply.

The dispatcher also applies user/account and IP restrictions. CRM sessions carrying a CSRF token must supply X-FMCSRF or _csrf; missing or incorrect tokens can be rejected before the handler runs.

Parameters

ParameterTypeRequiredDescription
idInteger stringYesPositive physical job ID used for invoice selection.
pandpNumeric stringNoOmitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected.
discountNumeric stringNoOmitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected.
discount_vatNumeric stringNoOmitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected.
pandp_vatNumeric stringNoOmitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected.
rowsNative array or JSON array stringNoOmitted preserves loaded rows. A valid native/JSON list replaces the complete invoice rows, not only this job’s rows. [] explicitly clears; blank, malformed, non-list and non-row data are rejected before writes. See row format below.
doc_noStringNoPresence writes the supplied value, including blank; omitted preserves it.
order_noStringNoSame presence semantics as doc_no.
invoice_numberStringNoTruthy input trimmed into num; blank or '0' does not clear it. No uniqueness validation here.
notesStringNoIf present, trims and replaces notes; blank clears.
cdateDate stringNoTruthy value; DD-MM-YYYY pattern is rewritten to YYYY-MM-DD, other text passed through. No strict calendar validation.
due_dateDate/time stringNoTruthy input passed to strtotime in PHP timezone; not an epoch-input contract. Blank cannot clear, and invalid truthy input is not safely rejected.
from_dateDate stringNoTruthy input stored as meta.invoice_from after DD-MM-YYYY rewrite; blank does not clear.
to_dateDate stringNoSame semantics, stored as meta.invoice_to.
nominal_codeStringNoNonblank trimmed accounting code. Resolves the corresponding local meta.nominalID; a valid conflicting nominalID/nominal_id is rejected.
nominalID / nominal_idInteger stringNoLocal account ID; nominalID takes precedence if both aliases are supplied. May be supplied alone to resolve the accounting nominal_code. A conflicting valid code/ID pair is rejected.
tax_code_idInteger stringNoPresence integer-casts and replaces tax_code_id, including 0; no compatibility validation.
form_idInteger stringNoDefault 0 preserves form. Nonzero replaces the form ID without validating its type, existence or archive state.

Example request

$result = FM_api('Job_invoiceUpdate', [
	'id' => '1042',
	'order_no' => 'PO-1042',
	'notes' => 'Inspection invoice',
]);

Example response

{"ok":1,"num":"INV-501","id":"501"}

Changed saves return the invoice number and ID. No-change returns {"ok":1,"msg":"no update needed"} without num/id. Client code must handle both shapes; IDs are typically database strings.

Errors and unusual responses

ResponseCondition
{"error":"not logged in"}No authenticated tenant context is available. Invalid signed credentials can be rejected by the dispatcher before the handler runs.
{"error":"This user does not have access to certain CRM features. Check user permissions."}User crm_access bit 64 is set.
{"error":"missing or invalid parameter: id"}Missing/nonpositive job ID.
{"error":"failed to create invoice","api_error":{"error":"Job 1042 does not exist"}}Auto-generation did not return an invoice id; api_error preserves its response.
{"error":"cannot edit invoices that are marked Paid"}Selected invoice status is exactly 2; other statuses are not blocked by this guard.
{"error":"Unable to save job delivery changes"}A checked database write, audit or transaction failed; local transactional writes roll back. Cross-tenant files/commits are not distributed-atomic.
{"error":"invalid parameter: rows"}Present rows are malformed, not a list or contain non-row/empty values; rejected before writes.

Legacy application errors may be returned with HTTP 200. Check the decoded response for error, not only the HTTP status.

Behaviour and edge cases

  • Selects the first active type-3 invoice without ordering, then an active legacy-linked invoice. With neither, calls Job_invoiceGenerate. No transaction encloses creation and subsequent edits.
  • Rows normalize through Invoice_lineItems: named fields include description, price, qty, net, taxpc, gross, nominal and itemcode; legacy positional rows are [description, price, qty, net, taxpc, gross, nominal, itemcode, taxtype]. Shared row helpers normalize tax/nominal aliases and remove empty rows. JSON-encode nested rows before signing with FM_api.
  • Row normalization fills missing net/netPrice from price times qty, resolves supplied tax IDs against tenant tax categories, and can replace taxpc and recalculate gross when that rate differs. Existing discountOverall participates in gross calculation; do not assume supplied gross is always retained. It also reconciles legacy nominal aliases to account ID/code fields.
  • With rows or any charge/tax adjustment supplied, recalculates invoice total_net/total from normalized net/gross rows, adds pandp/pandp_vat and subtracts discount/discount_vat. Without these inputs, preserves current totals.
  • All four charge/discount fields are independently compared, so offsetting changes that leave totals unchanged still persist.
  • Header, replacement rows, this job’s subtotal and history commit together in a checked transaction. Row storage errors roll back the header. This legacy route remains a whole-invoice row replacement, not a per-job cost save.
  • Changed saves recompute only this job’s row-owned price/price_net. Rows with explicit job_id/jobID count for that job; unowned rows count only if the invoice has one linked job. Header charges/discounts are not allocated into that job subtotal.
  • A missing/deleted selected invoice returns an error. The endpoint does not delete unrelated relation rows or silently switch invoices during the locked update.
  • No request version/last_edited conflict guard, list filtering or pagination applies. Explicit charge/discount values must be finite numbers; nominal_code and nominalID/nominal_id resolve to a canonical accounting-code/local-ID pair, with conflicting valid pairs rejected.

Side effects

  • May create/link an invoice first. Changed saves update invoice fields/meta/last_edited and edited_uuid=Job_invoiceUpdate, persist rows when changed, fire Invoice_edit and log job invoice update.
  • Updates the active job’s price/price_net and app-sync timestamps only when its calculated subtotal differs; invalidates the meetings cache. Other linked jobs are not recalculated here.

See it in Fieldmotion

CRM1

Edit the job invoice screen or invoice table widget. pageJobInvoice.js / widgetInvoiceTable.js debounce Job_invoiceUpdate saves; inspect whether rows is omitted (header edit) or sent in full.

CRM2

The legacy pageJobInvoice and widgetInvoiceTable/widgetTableJobToInvoiceLink still call Job_invoiceUpdate. Summary autosave can send only discount/discount_vat; current mapped invoice row editing is a separate path.

Related endpoints