Invoices
Add one invoice row
/a/InvoiceRow_add
Insert one normalized line item into an unpaid invoice without replacing its existing rows.
When to use this endpoint
Call InvoiceRow_list first, then use this route when a persisted invoice editor creates a new row.
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. |
position | Integer-compatible | No | Request value takes precedence over row.position. Blank/omitted appends; otherwise values below zero become 0 and existing rows at/after the position shift up. |
Example request
$result = FM_api('InvoiceRow_add', [
'invoice_id' => 981,
'row' => json_encode(['description' => 'Service', 'qty' => 2, 'price' => 50, 'taxpc' => 20]),
]);
Example response
{
"ok": 1,
"total_net": 100,
"total": 120,
"row": {"description":"Service","qty":2,"price":50,"net":100,"gross":120,"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":"cannot edit invoices that are marked Paid"} | Invoice status is Paid. |
{"error":"invoice row job is not linked to invoice; reload before editing"} | A positive job_id is not directly/legacy linked to the invoice. |
{"error":"...refer to different accounts..."} | Nominal ID/code aliases conflict. |
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 operation waits up to about 15 seconds for the invoice-row lock; contention returns
invoice rows are being saved. please try again soon.. - The first targeted row operation lazily copies legacy embedded rows to the row table and sets
meta.rows_storage=table. - Empty rows are rejected. Missing net defaults from price × quantity, tax selection can override
taxpc, and gross is derived when absent or tax changes.
Side effects
- Inserts a row-table record, recalculates invoice net/gross totals and edit metadata, fires
Invoice_edit, and updates price totals/edit metadata on linked jobs. - Once table storage is canonical, targeted mutations do not rewrite the legacy
invoices.rowscache; readers use the storage marker.
See it in Fieldmotion
CRM1
Job Invoice, invoice line-item widgets, and the main Invoices editor call this route when a new saved row is edited.
CRM2
Configurable invoice and Job Costs table widgets use this route for a new persisted row and include the linked job ID where applicable.
Related endpoints
InvoiceRow_listsupplies stable row IDs and positions before a targeted edit.Invoice_setRowsreplaces the complete row collection for compatibility callers.