Factuarea APIDevelopers
Added

Counted units, weight by pieces and base unit replacement

Sell in packs, boxes or pieces over a product measured in kilos or litres, record the pieces on variable-weight lines, replace a product by one with a different base unit with a preview and an irreversible execution, and read the real unit in FacturaE and UBL. Two new operations, two MCP tools, the product.replaced event and new fields on products and document lines.

15 October 2026 — A product measured in kilos, litres or metres can now be sold in counted units such as packs, boxes or pieces, and a product that already has history can change its base unit by being replaced by a new one. Everything is additive: no existing field changes meaning.

Counted units

There are 22 unit codes in total: 9 measure units and 13 counted units.

KindUnits (UN/ECE code)Where it can be used
Measureunit C62, kilogram KGM, gram GRM, litre LTR, millilitre MLT, metre MTR, square metre MTK, hour HUR, day DAYBase unit, presentations, supplier offers, price lists and document lines
Countedpiece H87, pack XPK, box XBX, tray XDS, bag XBG, bottle XBO, demijohn XDJ, can XCA, tub XTB, jar XCI, drum XJY, envelope XEN, case XCSPresentations, supplier offers, price lists and document lines

base_unit only accepts the 9 measure units; the counted units are commercial units.

  • Presentations and supplier offers. A fixed presentation in a counted unit can rest on a measured base if it declares its equivalence (conversion_factor or nominal_base_quantity): «1 pack = 0.5 kg». A measure unit must belong to the same family as the base. A presentation or an offer whose unit cannot be expressed over the base unit of the product is rejected with 422 unit_not_expressible.
  • Weight by pieces. A variable-weight presentation whose unit is a counted unit accepts the pieces the customer asked for in counted_quantity. The line is still charged, and the stock still discounted, by quantity, the real weight in the base unit; the pieces are informational.
  • Documents. Invoice, quote, pro-forma, delivery note and recurring invoice lines carry counted_quantity and counted_unit, both null when the line has no counted pieces. The editor, the PDF, the ticket and the public link show «4 packs = 2 kg» or «3 pieces · 1.520 kg × 4.55 €/kg».
  • FacturaE and UBL. FacturaE declares the real UnitOfMeasure of each line and UBL sends its real UN/ECE unitCode.

Base unit replacement

The base unit of a product with history (stock, movements, presentations, supplier offers or variants with stock) is not edited in place. The product now reports it in base_unit_change, and the change is made by replacing the product by a new one.

  • Preview a base unit replacement computes, without writing anything, the plan and an impact_token that confirms that exact plan. You send the new base unit (target_base_unit, one of the 9 measure units), the equivalence between both units, an optional base_price and one keep or retire decision per presentation.
  • Replace a product by changing its base unit executes it and answers 201 with the new product. It is irreversible, so Idempotency-Key is mandatory. The stock, the presentations you keep, the variants, the supplier offers, the price-list entries and the pack components move to the new product, converted to its unit. The previous product is deactivated, keeps its whole history and can never be reactivated. It does not consume plan quota.
  • If the conversion loses precision or has no equivalence, base_price must be confirmed (422 base_unit_replacement_price_confirmation_required). If the catalog, the price lists or the packs change after the preview, the confirmation is rejected with 422 base_unit_replacement_preview_stale and a new preview is requested.
  • The other rejections of the replacement are 422 business_rule_violation with subcode product_already_replaced, base_unit_replacement_conversion_unavailable or base_unit_replacement_blocked_by_packs, and 422 parameter_invalid_value with subcode base_unit_replacement_same_unit (the target is already the base unit of the product), base_unit_replacement_equivalence_invalid or base_unit_replacement_decision_invalid.
  • The equivalence is the number of units of the current base that equal one unit of the new one. It is required between a counted unit and a measure unit, in either direction; within the same measure family it is calculated by itself, and between two different measure families there is none.
  • A product that takes part in a replacement, as the previous product or as the new one, cannot be deleted: Delete a product and Delete multiple products in bulk answer 422 with subcode product_in_base_unit_replacement, and in the bulk operation the product is listed in failed[] with that reason. The MCP tools delete_product and bulk_delete_products apply the same rule.
  • Editing base_unit on a product whose base_unit_change.mode is replacement answers 422 business_rule_violation with subcode product_base_unit_locked.

Fields, events and MCP

  • Product. base_unit_change (mode and the quantified blockers), replaces and replaced_by (id, name, base_unit, replaced_at, equivalence and stock_transferred). They are returned on single-product reads and omitted in lists.
  • Event. product.replaced is published with the new product as its object. See Events.
  • Stock movements. The reasons base_unit_replacement_out and base_unit_replacement_in and the source base_unit_replacement record the transfer between both products.
  • Price lists. The withdrawal reason product_replaced identifies the entries of the replaced product.
  • MCP. Two new tools, preview_product_base_unit_replacement and replace_product_base_unit, both with the products:write scope; the second is irreversible and accepts idempotency_key. See the tool catalog.

Pack components do not accept pieces: components.*.counted_quantity answers 422 when it carries a value, because pieces belong to the lines of sales documents.

The operations that read or write products, presentations, supplier offers, price lists and sales documents keep their scopes, and their responses and bodies gain the fields and unit codes described above.

New endpoints2

EndpointDescription
POST/v1/products/{product}/base_unit_replacement/previewPreview a base unit replacement
POST/v1/products/{product}/base_unit_replacementReplace a product by changing its base unit

Updated endpoints103

EndpointDescription
POST/v1/proformas/{proforma}/acceptAccept a proforma
POST/v1/quotes/{quote}/acceptAccept a quote
POST/v1/recurring_invoices/{recurring_invoice}/activateActivate recurring invoice
POST/v1/invoices/{invoice}/annulAnnul an invoice
POST/v1/invoices/{invoice}/assign-real-numberAssign a real invoice number
POST/v1/delivery_notes/{delivery_note}/cancelCancel a delivery note
POST/v1/recurring_invoices/{recurring_invoice}/cancelCancel recurring invoice
POST/v1/delivery_notes/{delivery_note}/convertConvert delivery note to invoice
POST/v1/proformas/{proforma}/convertConvert proforma to invoice
POST/v1/quotes/{quote}/convertConvert quote to invoice
POST/v1/invoices/{invoice}/correctiveGenerate corrective invoice
POST/v1/delivery_notesCreate a delivery note
GET/v1/delivery_notesList all delivery notes
POST/v1/invoicesCreate an invoice
GET/v1/invoicesList all invoices
POST/v1/price-listsCreate a price list
GET/v1/price-listsList price lists
POST/v1/productsCreate a product
GET/v1/productsList all products
POST/v1/products/{product}/presentationsCreate a product presentation
GET/v1/products/{product}/presentationsList product presentations
POST/v1/proformasCreate a proforma
GET/v1/proformasList all proformas
POST/v1/purchase_invoicesCreate an expense
POST/v1/quotesCreate a quote
GET/v1/quotesList all quotes
POST/v1/invoices/{invoice}/create-recurringCreate a recurring invoice from an invoice
POST/v1/recurring_invoicesCreate a recurring invoice
GET/v1/recurring_invoicesList all recurring invoices
POST/v1/products/{product}/supplier-offersCreate a supplier offer
GET/v1/products/{product}/supplier-offersList supplier offers
POST/v1/webhook_endpointsCreate a webhook endpoint
GET/v1/webhook_endpointsList all webhook endpoints
GET/v1/delivery_notes/{delivery_note}Retrieve a delivery note
PUT/v1/delivery_notes/{delivery_note}Update a delivery note
GET/v1/invoices/{invoice}Retrieve an invoice
PUT/v1/invoices/{invoice}Update an invoice
GET/v1/price-lists/{priceList}Retrieve a price list
PUT/v1/price-lists/{priceList}Update a price list
GET/v1/products/{product}Retrieve a product
PUT/v1/products/{product}Update a product
DEL/v1/products/{product}Delete a product
POST/v1/products/bulk-deleteDelete multiple products in bulk
PUT/v1/products/{product}/presentations/{presentation}Update a product presentation
POST/v1/products/{product}/videoUpload a video to a product
GET/v1/proformas/{proforma}Retrieve a proforma
PUT/v1/proformas/{proforma}Update a proforma
PUT/v1/purchase_invoices/{purchase_invoice}Update an expense
GET/v1/quotes/{quote}Retrieve a quote
PUT/v1/quotes/{quote}Update a quote
GET/v1/recurring_invoices/{recurring_invoice}Retrieve a recurring invoice
PUT/v1/recurring_invoices/{recurring_invoice}Update a recurring invoice
PUT/v1/products/{product}/supplier-offers/{offer}Update a supplier offer
GET/v1/webhook_endpoints/{webhook_endpoint}Retrieve a webhook endpoint
PUT/v1/webhook_endpoints/{webhook_endpoint}Update a webhook endpoint
POST/v1/delivery_notes/{delivery_note}/duplicateDuplicate a delivery note
POST/v1/invoices/{invoice}/duplicateDuplicate an invoice
POST/v1/proformas/{proforma}/duplicateDuplicate a proforma
POST/v1/quotes/{quote}/duplicateDuplicate a quote
POST/v1/delivery_notes/find-by-external-idFind a delivery note by external ID
POST/v1/invoices/find-by-external-idFind an invoice by external ID
POST/v1/invoices/find-by-numberFind an invoice by number
POST/v1/products/find-by-external-idFind a product by external ID
POST/v1/products/find-by-skuFind a product by SKU
POST/v1/proformas/find-by-external-idFind a proforma by external ID
POST/v1/quotes/find-by-external-idFind a quote by external ID
POST/v1/recurring_invoices/find-by-external-idFind a recurring invoice by external ID
GET/v1/products/low-stock-reportList products below the stock threshold
POST/v1/invoices/{invoice}/issueIssue an invoice
GET/v1/eventsList all events
GET/v1/invoices/{invoice}/correctivesList corrective invoices
GET/v1/price-lists/{priceList}/itemsList price list items
POST/v1/price-lists/{priceList}/itemsUpsert a price list item
GET/v1/price-lists/optionsList active price list options
GET/v1/products/{product}/stock-movementsList stock movements of a product
GET/v1/webhook_endpoints/{webhook_endpoint}/deliveriesList webhook deliveries
POST/v1/delivery_notes/{delivery_note}/mark-deliveredMark delivery note as delivered
POST/v1/invoices/{invoice}/mark-paidMark invoice as paid
POST/v1/invoices/{invoice}/mark-sentMark an invoice as sent
POST/v1/recurring_invoices/{recurring_invoice}/pausePause recurring invoice
POST/v1/price-lists/{priceList}/items/{item}/reassignReassign a retired price list item
POST/v1/proformas/{proforma}/rejectReject a proforma
POST/v1/quotes/{quote}/rejectReject a quote
PATCH/v1/invoices/{invoice}/rescheduleReschedule an invoice
POST/v1/recurring_invoices/{recurring_invoice}/resumeResume recurring invoice
POST/v1/webhook_endpoints/{webhook_endpoint}/rotate_secretRotate webhook secret
POST/v1/invoices/{invoice}/scheduleSchedule an invoice
GET/v1/products/searchSearch products
POST/v1/invoices/{invoice}/sendSend invoice by email
POST/v1/proformas/{proforma}/sendSend proforma by email
POST/v1/quotes/{quote}/sendSend quote by email
POST/v1/webhook_endpoints/{webhook_endpoint}/test_eventSend a test event
POST/v1/products/{product}/supplier-offers/{offer}/preferredSet the preferred supplier offer
GET/v1/events/{event}Retrieve an event
GET/v1/webhook_endpoints/{webhook_endpoint}/deliveries/{delivery}Retrieve webhook delivery
POST/v1/delivery_notes/{delivery_note}/signSign a delivery note
POST/v1/recurring_invoices/{recurring_invoice}/skipSkip the next recurring invoice generation
POST/v1/invoices/substitute-simplifiedSubstitute simplified invoices with full invoice
POST/v1/products/{product}/toggle-activeToggle product active state
POST/v1/invoices/{invoice}/unscheduleUnschedule an invoice
POST/v1/invoices/{invoice}/unsendUnsend an invoice
PUT/v1/products/{product}/stockUpdate product stock
POST/v1/invoices/{invoice}/voidVoid an invoice

On this page

Need a hand?Contact support