Factuarea APIDevelopers
Contract

Measured purchase lines

Purchase invoice lines now record the packages received, the unit the supplier cost applies to and the real measured quantity. The contract adds a measurement block, persists line discounts, resolves the cost from the supplier offer and duplicates a purchase from an authorised source.

Purchase invoices now keep the physical side of a purchase apart from the billed side. A line can state how many packages arrived, which unit the supplier cost is quoted in and the real measured quantity; the server derives the billed quantity, the conversion factor to the base unit and the frozen unit cost. A line without the new block keeps its previous quantity × unit_price semantics.

New request fields

POST /v1/purchase_invoices and PUT /v1/purchase_invoices/{purchase_invoice} accept these additive fields. Omitting all of them is still a valid request.

FieldMeaning
source_purchase_invoice_idUUID of a purchase invoice of your company whose historical lines you want to duplicate. Create only. The key needs purchase_invoices:read on top of purchase_invoices:write, otherwise 403 insufficient_scope; an unknown or foreign source returns 404 purchase_invoice_not_found.
lines[].purchase_measurementClosed object with package_quantity (decimal string or null) and cost_basis (offer_unit, base_unit or presentation_unit). Omitted or null keeps the legacy contract. The derived read fields (billing_*, unit_cost) are rejected on write, even when copied from a previous read.
lines[].discount_percentPercentage discount, 0–100 with up to four decimals, applied before tax. The API used to accept it and drop it silently; it is now persisted and returned.
lines[].source_line_indexZero-based ordinal of the line in the authorised source document. It keeps a duplicated line matched to its historical measures and frozen cost. Never an internal key.
lines[].price_sourcemanual or supplier_offer. New on the public API; it changes how unit_price is read (see below).
metadata.purchase_source_reviewReserved key: a compact JSON string of at most 500 characters with the OCR reconciliation state (pending, reconciled or accepted_difference), source and calculated totals, difference and reason. Metadata stays flat; nested objects are rejected.

Example: three 2 kg boxes bought at a per-kilo offer

The supplier quotes 7.40 per kg. Three boxes of 2 kg are billed as 6 kg; the server derives the same gross amount, 44.40, whichever basis you choose.

{
  "external_invoice_number": "F-2026-0918",
  "issued_on": "2026-09-18",
  "supplier_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a10",
  "lines": [
    {
      "product_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
      "presentation_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02",
      "supplier_offer_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03",
      "quantity": "6.0000",
      "unit_price": "7.4000",
      "price_source": "supplier_offer",
      "tax_rate": 10,
      "purchase_measurement": { "package_quantity": "3.0000", "cost_basis": "offer_unit" }
    }
  ]
}

The same purchase priced per box uses "quantity": "3.0000", "unit_price": "14.8000" and "cost_basis": "presentation_unit" with a manual cost. A weighed presentation (variable measure) also requires confirmed_base_quantity, even when an offer applies.

How the cost is resolved

Two behaviours change for catalog lines (those carrying product_id) on the public API and MCP, matching the web application:

  • unit_price omitted means "cost not confirmed", not zero. The server proposes the cost of the applicable supplier offer. When the supplier has no active offer for the selected product or variant, the line is rejected with 422 parameter_invalid_value and param: unit_price. Previously that silence was stored as 0.00 and the stock entered valued at zero. Manual lines without product_id still require unit_price.
  • price_source: "supplier_offer" is no longer decorative. Sent together with unit_price, the amount is read as the echo of the cost the server resolved and the offer cost is frozen again. To impose a cost, send price_source: "manual" or omit it: the amount you send is respected.

The supplier_offer cost applies to the purchase unit of the offer. A purchase of variable measure without confirmed_base_quantity is rejected even with an offer. Historical documents are not recalculated.

New response fields

PurchaseInvoiceLine, returned by list, show, create and update:

FieldMeaning
purchase_measurementDerived block or null: package_quantity, billing_quantity, billing_unit_code, billing_conversion_factor, unit_cost and cost_basis.
confirmed_base_quantityReal measured quantity in the base unit as a decimal string; null when the line does not require one.
discount_percentApplied before tax. subtotal is already net of it, so unit_price × quantity does not equal subtotal on a discounted line.
source_line_indexZero-based ordinal of the line within its document, in stable read order.
quantity, unit_priceThe type is now number | string. A measured line returns 4-decimal strings so no precision is lost; a legacy line keeps the number it has always published. Parse both.

Errors

Measurement errors are 422 with code: parameter_invalid_value and subcode: catalog_line_assembly_invalid; param points at the field to fix.

paramCause
purchase_measurement.cost_basisThe basis is invalid, the offer or presentation it requires is missing, or the purchase unit and the base unit belong to different families.
purchase_measurement.package_quantityPackages must be positive, with at most four decimals.
quantityThe billed quantity does not match the packages or the confirmed measure.
confirmed_base_quantityRequired on a variable-measure presentation, or the measure cannot be expressed with four decimals in the billing unit.
unit_priceNo cost to apply: the supplier has no active offer for the selection, or a legacy conversion would lose precision.

A stored measurement block that fails validation is reported with subcode: catalog_line_snapshot_invalid. See Errors.

MCP

create_purchase_invoice and update_purchase_invoice publish the same line schema, including purchase_measurement, discount_percent, price_source, source_line_index and source_purchase_invoice_id, and apply the same additional purchase_invoices:read rule when duplicating. get_purchase_invoice returns the new line fields. See the MCP tool catalog.

Read Product catalog for supplier offers and Amounts and dates for the per-line purchase fields.

Updated endpoints4

EndpointDescription
POST/v1/purchase_invoicesCreate a purchase invoice
PUT/v1/purchase_invoices/{purchase_invoice}Update a purchase invoice
GET/v1/purchase_invoices/{purchase_invoice}Retrieve a purchase invoice
GET/v1/purchase_invoicesList all purchase invoices

On this page

Need a hand?Contact support