Invoices
Replace invoice line items
/a/Invoice_setRows
Replace all line items on an unpaid invoice and atomically recalculate invoice and linked-job totals.
When to use this endpoint
Use this compatibility route for whole-list replacement. Prefer InvoiceRow_* endpoints for targeted table-backed row edits.
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 |
|---|---|---|---|
id | Positive integer | Yes | Active invoice ID. |
rows | JSON array string | Yes | Complete replacement row list. Empty row structures are normalized out; row nominal ID/code conflicts are rejected. |
Example request
$result = FM_api('Invoice_setRows', [
'id' => 981,
'rows' => json_encode([['description' => 'Service', 'qty' => 1, 'price' => 100, 'taxpc' => 20]]),
]);
Example response
{
"ok": 1
}
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":"invalid parameter: rows"} | The value is not a JSON array. |
{"error":"cannot edit invoices that are marked Paid"} | The invoice is Paid. |
{"error":"...refer to different accounts..."} | A line supplies conflicting nominal identifiers. |
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 replacement runs under an invoice row lock, making concurrent whole-list saves serialize. Identical normalized rows return
warning: no change necessary. - Storage may migrate to
invoices_rowswhile the legacy JSON cache remains compatible.
Side effects
- Replaces rows, recalculates net/gross totals and linked-job price totals, advances edit metadata, and fires
Invoice_edit.
See it in Fieldmotion
CRM1
No current direct CRM1 string caller was found; legacy integrations and regression tests use this compatibility route.
CRM2
No current direct production CRM2 caller was found; mapped saves use Invoice_updateData and targeted edits can use InvoiceRow_*.
Related endpoints
Invoices_getDataTablesupplies paginated CRM2 invoice rows and IDs.Invoices_getDTis the CRM1 invoice-list counterpart.