FurniOpsDocs

Master records and provenance

How many vendors' descriptions of a product become one master record, what each field remembers about its origin, and why IDs never change.

A vendor's API, its price list and a distributor's feed can all describe the same sofa, each a little differently. FurniOps keeps what every source says, then builds one master record from it: the product as FurniOps believes it is, field by field. That record is what the API returns and what events describe.

Products, variants and offers

Product   prod_…   title, brand, description, category, media …
 └─ Variant  var_…   sku, gtin, colour, finish, dimensions, cartons …
     └─ Offer   ofr_…   one source's price, stock and availability at one location
  • A product is the thing a shopper recognises: the Harbor 3-Seat Sofa.
  • A variant is one sellable version of it: the Oatmeal fabric, with its own SKU, GTIN, dimensions and cartons.
  • An offer is one source's terms for a variant at one location: cost, msrp, map, price, compare_at, quantity, incoming stock with ETAs, availability and lead_time_days, stamped with observed_at, when the source said it.

Offers change far more often than content, so they are kept apart and have their own fast path and their own event, offer.changed. A price move never rewrites the product.

Field-level provenance

Every value in a master record knows where it came from. When a field changes, the event carries its provenance:

{
  "path": "cost",
  "old": 89900,
  "new": 94900,
  "provenance": {
    "source": "src_034XqBaL2mRLTZPXyRbaUh",
    "method": "mapped",
    "confidence": 1,
    "observed_at": "2026-10-04T10:00:00Z"
  }
}
FieldMeaning
sourceThe source whose value won.
methodHow the value was obtained: mapped (read directly from a mapped column), transformed (converted, such as units), inferred (derived from other data), ai (extracted by a model), operator (set by a person).
confidence0 to 1: how sure FurniOps is of the value.
observed_atWhen the source stated it, not when FurniOps stored it.

When sources disagree, each field is decided on its own: by which source is preferred for that field (a vendor's API over a scraped page for price, say), then by how recent the observation is, then by confidence. The newest observation wins, not the newest arrival, so a slow, stale feed cannot overwrite a fresher value.

Versions

Products, variants and offers each carry a version that only grows, and every event names the version it produced. Keep the version you last applied, and apply a change only when its version is newer: then retries, replays and out-of-order deliveries can never take your copy backwards.

Status, absence and discontinuation

status is one of active, probably_discontinued, discontinued or merged. Nothing is deleted because a source left it out once: a product a source stops listing moves to probably_discontinued, then to discontinued only after it has stayed absent across several runs, and a pull that returns far less than usual is treated as a broken pull, not a mass discontinuation. product.discontinued tells you when a product is gone.

Merges keep IDs stable

A product's ID never changes. When FurniOps finds that two products are the same (often from two sources), it merges them: the survivor keeps its ID, and the other product publishes product.merged with merged_into, the survivor's ID, and takes status merged. Follow the redirect in your copy.

A merge can be undone. product.unmerged names unmerged_from and lists every field again, so you can rebuild the product exactly. Variants merge and unmerge the same way (variant.merged, variant.unmerged). How FurniOps decides two records are one item is in How matching works; review pairs and merges are in Review and merges.

Money and measurements

  • Money is an integer number of minor units with a lowercase ISO 4217 currency: 94900 with usd is $949.00. Never a float.
  • Measurements carry their value and unit, and FurniOps keeps the unit the source used, so nothing is lost in conversion.

On this page