Factuarea APIDevelopers

Product catalog

Model products and services with UNECE units, presentations, variants, supplier offers and immutable document snapshots.

The product catalog models what you sell and, separately, how it is packaged, identified, stocked and purchased. Start with a fact_test_ key and grant only products:read and/or products:write as needed.

Product or service

Every item has an explicit item_kind:

  • product can manage decimal stock and have stock-owning variants.
  • service always has manage_stock: false, stock 0.0000 and no low-stock threshold. A service cannot have a variant with its own stock.

The base unit is one of the closed UNECE catalog: C62, KGM, GRM, LTR, MLT, MTR, MTK, HUR or DAY. Quantities and stock use up to four decimals; conversion factors use up to six. You cannot change the base unit once existing stock, presentations, variants, offers or stock history would be reinterpreted.

{
  "name": "Extra-virgin olive oil",
  "sku": "EVOO-BULK",
  "price": "8.75",
  "item_kind": "product",
  "base_unit": "LTR",
  "manage_stock": true,
  "stock": "125.5000",
  "low_stock_threshold": "20.0000",
  "specifications": { "origin": "Jaén", "harvest": 2026 }
}

Create and update semantics

POST /v1/products requires name and price. If external_id already identifies a product in the same company, creation is an idempotent upsert instead of a duplicate. sku is also unique per company, but a collision with another product is an error rather than an upsert.

PUT /v1/products/{product} has partial semantics: every omitted field keeps its current value. Two fields have explicit replacement behavior:

  • specifications replaces the complete key-value object; null clears it. Non-empty JSON lists are rejected, while {} and [] remain compatible empty representations.
  • stock is an absolute quantity, not a delta. null sets it to zero. A real difference records a manual stock movement; sending the current value records nothing. Use the dedicated stock endpoint with increase or decrease for explicit deltas.

stock and low_stock_threshold accept up to four decimals. Product metadata keeps its backwards-compatible boundary: up to 50 keys, scalar values are coerced to strings, and JSON lists are accepted and persisted with numeric keys. This is intentionally different from specifications.

Presentations

A presentation is a commercial way of selling a product; it never owns stock.

ModeMeaning
fixedbase quantity = commercial quantity × conversion_factor. A box of 12 units uses factor 12.000000.
variable_measureThe nominal measure is informative. Each document line must confirm its real base quantity.
POST /v1/products/{product}/presentations
{
  "name": "Box of 12",
  "mode": "fixed",
  "unit": "C62",
  "conversion_factor": "12.000000",
  "active": true
}

Use the collection GET/POST endpoint and the item PUT/DELETE endpoint. An inactive presentation cannot be selected for a new line, but old documents continue to render from their snapshot.

Variants

A variant carries its own commercial identity: name, optional SKU/barcode, price and cost overrides, and optionally its own stock.

{
  "name": "Blue / M",
  "sku": "TSHIRT-BLU-M",
  "price_override": "24.5000",
  "manage_stock": true,
  "stock": "8.5000",
  "active": true
}

When manage_stock is true, movements affect only the variant; otherwise they fall back to the base product while preserving the variant identity in the line and stock ledger. A presentation can be combined with a variant: the variant chooses the stock target and the presentation converts the quantity.

Configurable options and sellable combinations

Option groups model choices a buyer selects — finish, flavour or service level — rather than informational specifications. Each group contains single-select values; a value can add a price_adjustment per commercial unit. Inactive groups/values stay readable so historical line snapshots keep their labels, but cannot be selected for a new line.

Commercial configurations combine an optional variant, an optional presentation and a canonical set of option values. Each configuration has a stable signature, can define a final price per commercial unit and can be active or inactive.

catalog_availability_mode controls what is sellable:

ModeAvailability
openThe Cartesian product of active axes is sellable. A configuration only restricts availability when restricts_availability: true; it constrains its named variant and treats omitted presentation/options as wildcards.
closedActive configurations are the exact allow-list. With no active configuration, nothing is sellable.

Creation defaults to open; omitting the field on update preserves the current mode. Configuration rows with restricts_availability: false can still provide identity or an exact price without narrowing an open catalog.

Use GET /v1/products/{product}/options and GET /v1/products/{product}/configurations to inspect the catalog. Both use products:read and cursor pagination. Product detail can include both with include=configurable_catalog.

POST /v1/products/{product}/resolve-selection is also a read, despite POST: it accepts a nested partial selection and returns resolved, incomplete, ambiguous or incompatible, plus the remaining compatible axes and pending groups. Duplicate values from the same group are an invalid request.

Before a write narrows availability, call POST /v1/products/{product}/configurations/impact-preview with products:write. It persists nothing and returns the affected combinations, retired negotiated prices and an impact_token that fingerprints that exact catalog state. Send the token back on the destructive update; if the catalog or affected prices changed meanwhile, request a new preview.

Stock ledger

GET /v1/products/{product}/stock-movements exposes the append-only stock ledger from newest to oldest. Each row has the signed base-unit delta, the stock_after balance calculated from the complete ledger, reason, source document, acting user and managing variant. Cursor pagination uses limit and starting_after; direction=in|out filters visible rows without changing the historical stock_after values.

Supplier offers

A supplier offer represents purchase terms without changing the sales price. It points to the product or one variant and records the supplier, purchase unit, conversion factor, availability, cost and lead time.

{
  "supplier_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a10",
  "variant_id": null,
  "supplier_sku": "SUP-EVOO-20L",
  "purchase_unit": "LTR",
  "conversion_factor": "1.000000",
  "unit_cost": "5.1250",
  "availability": "available",
  "minimum_quantity": "20.0000",
  "lead_time_days": 3,
  "preferred": true,
  "active": true
}

available requires a non-negative unit_cost. unavailable, unknown, seasonal and store_dependent require unit_cost: null. Only one active offer can be preferred for a product/variant target; use the dedicated preferred action to switch it atomically.

When a purchase invoice line buys against an offer, purchase_measurement records the packages received and the unit the cost is quoted in (offer_unit, base_unit or presentation_unit); the server derives the billed quantity and freezes the unit cost. unit_price omitted on a catalog line means "cost not confirmed": the API applies the offer cost or rejects the line when the supplier has no active offer. See Measured purchase lines.

Documents keep snapshots

Catalog-backed document lines return the selected product_id, variant_id, presentation_id, commercial configuration/signature and option values, item_kind, commercial unit, base unit, conversion factor, base quantity, price source/semantics, option adjustments, price-list identity and price unit. These are snapshots: changing or retiring the live catalog does not rewrite issued or historical documents.

For a variable_measure presentation, send confirmed_base_quantity when building the line. Let the API derive totals and stock movement quantities.

Permissions, deletion and MCP

  • Reads use products:read; creates, updates and the new subresource deletes use products:write. Deleting the base product uses products:delete.
  • Presentation, variant and supplier-offer deletes are marked x-irreversible: true in OpenAPI.
  • Products referenced by a live price-list item or supplier offer cannot be hard-deleted. Remove the active reference first; document history is safe in snapshots.
  • The Product MCP domain now has 38 tools. In addition to the existing 13 presentation/variant/supplier-offer tools, four new reads expose options, configurations, stock movements and selection resolution.

See Price lists, Scopes & irreversibility and the exact schemas in the API Reference.

Document descriptions and reports

Document lines support nullable additional_description up to 5000 characters alongside the main description. It is available on sales invoices and correctives, quotes, proformas, delivery notes, purchase invoices and recurring templates. Keep it separate from quantities, prices, tax fields and catalog identifiers; adding detail does not change the amount calculation. Read GET /v1/products/low-stock-report for the collection and total_count, and GET /v1/products/{product}/sales-analytics for sales metrics; these responses are not single product resources. See their complete examples in the API Reference.

On this page

Need a hand?Contact support