Invoices
Edit one invoice row
/a/InvoiceRow_edit
Replace one persisted invoice line item while preserving its invoice and established job ownership.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
invoice_id / id | Positive integer-compatible | Yes | Active invoice ID. invoice_id takes precedence over the legacy id alias. |
row | Object or JSON object string | Yes | Line 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_id | Positive integer-compatible | Normally | Existing row ID. Request aliases take precedence over the row property. |
position | Integer-compatible | If no positive row ID | Request 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
| Response | Condition |
|---|---|
{"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_listsupplies stable row IDs and positions before a targeted edit.Invoice_setRowsreplaces the complete row collection for compatibility callers.