When to use this endpoint

Load rows first and send the returned positive row ID. Use position only as a legacy fallback.

Authentication and permissions

The request must use a logged-in CRM session or valid signed API credentials and is scoped to that tenant.

Users whose legacy crm_access mask contains bit 64 receive {"error":"no permission to access api"}. No record-owner check is made.

Parameters

ParameterTypeRequiredDescription
invoice_id / idPositive integer-compatibleYesActive invoice ID. invoice_id takes precedence over the legacy id alias.
rowObject or JSON object stringYesLine item. Common fields include description, price, qty, net/netPrice, discountOverall, gross, taxpc, tax and nominal IDs/codes, itemcode, notes, source IDs, and optional job_id.
row_id / invoice_row_id / row.invoice_row_idPositive integer-compatibleNormallyExisting row ID. Request aliases take precedence over the row property.
positionInteger-compatibleIf no positive row IDRequest value takes precedence over row.position and resolves the existing row. It is also stored as the replacement position.

Example request

$result = FM_api('InvoiceRow_edit', [
	'invoice_id' => 981,
	'row_id' => 441,
	'row' => json_encode(['description' => 'Updated service', 'qty' => 2, 'price' => 55, 'job_id' => 2204]),
]);

Example response

{
  "ok": 1,
  "total_net": 110,
  "total": 110,
  "row": {"description":"Updated service","qty":2,"price":55,"job_id":2204,"invoice_row_id":441,"position":0}
}

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":"invoice row does not exist"}Neither the scoped row ID nor legacy position identifies an active row.
{"error":"invoice row belongs to another job; reload before editing"}An already job-owned row is reassigned or unassigned.
{"error":"invoice row job is not linked to invoice; reload before editing"}An unassigned row is assigned to a job not linked to the invoice.
{"error":"cannot edit invoices that are marked Paid"}Invoice status is Paid.

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

Behaviour and edge cases

  • The entire normalized row replaces the prior row content; omitted row fields are not merged from the old row.
  • An owned row may retain only the same positive job ID. An unassigned row may remain unassigned or adopt a directly/legacy linked job.
  • The operation uses the same lock, normalization, lazy hydration, and nominal-conflict rules as add.

Side effects

  • Updates indexed row fields and JSON, recalculates invoice totals/edit metadata, fires Invoice_edit, and recalculates totals for current and former row-owning jobs.

See it in Fieldmotion

CRM1

Job Invoice, invoice table widgets, and the main Invoices editor call this route as persisted rows change.

CRM2

Configurable invoice and Job Costs table widgets call this endpoint with the stable row ID loaded by InvoiceRow_list.

Related endpoints

  • InvoiceRow_list supplies stable row IDs and positions before a targeted edit.
  • Invoice_setRows replaces the complete row collection for compatibility callers.