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,incomingstock with ETAs,availabilityandlead_time_days, stamped withobserved_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"
}
}| Field | Meaning |
|---|---|
source | The source whose value won. |
method | How 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). |
confidence | 0 to 1: how sure FurniOps is of the value. |
observed_at | When 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:94900withusdis $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.