When to use this endpoint

Use this when copying/importing multiple rows into an existing invoice. It avoids repeated lock and total-recalculation work.

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.
rowsArray or JSON array stringYesRows appended after the current maximum position. Empty line structures are discarded; an empty array is accepted and returns no row IDs.

Example request

$result = FM_api('InvoiceRow_appendMany', [
	'invoice_id' => 981,
	'rows' => json_encode([
		['description' => 'Part A', 'qty' => 1, 'price' => 20],
		['description' => 'Part B', 'qty' => 2, 'price' => 15],
	]),
]);

Example response

{
  "ok": 1,
  "total_net": 50,
  "total": 50,
  "row_ids": [441, 442]
}

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":"cannot edit invoices that are marked Paid"}Invoice status is Paid.
{"error":"missing or invalid parameter: rows"}Rows do not decode to an array.
{"error":"...refer to different accounts..."}A row has conflicting nominal ID/code aliases.
{"error":"invoice row job is not linked to invoice; reload before editing"}A surviving row names an unrelated job; the batch is rejected.

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 shares the same normalization, lazy hydration, 15-second lock wait, total recalculation, event, and linked-job updates as single-row add. Returned row_ids follow the surviving normalized row order.
  • Every positive row job_id must identify a directly/legacy linked job, as for InvoiceRow_add. Unassigned rows are allowed. All surviving rows are checked before hydration or batch inserts, so an unrelated owner rejects the entire batch without adding its valid prefix or changing totals.

Side effects

  • Appends row-table records, recalculates invoice totals/edit metadata, fires Invoice_edit, and recalculates candidate linked-job totals.

See it in Fieldmotion

CRM1

The invoice table widget uses this route when copying a group of prepared rows into a job invoice.

CRM2

Quote-to-invoice line-item copying and invoice-table workflows call this batch route.

Related endpoints

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