FurniOpsDocs

React to price and stock changes

Use offer.changed to update prices and stock within seconds, tell price moves from stock moves, and check stock at the moment that matters.

Every change to a vendor's price, quantity, incoming stock or availability is one offer.changed event. It is the busiest event FurniOps sends and the one most integrations care about: repricing, low-stock alerts, hiding what cannot ship.

Subscribe to offers only

Create an endpoint for just the offer events, so price and stock changes are not queued behind content updates in your handler:

curl https://api.furniops.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $FURNIOPS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/offers",
    "event_types": ["offer.created", "offer.changed"],
    "coalesce_seconds": 30
  }'

coalesce_seconds (0 to 300) combines a burst: changes to one offer still waiting to be sent are merged into a single event, with each field's first old and last new. A vendor that rewrites its price list in three passes then costs you one event, not three. The price is that each delivery waits the window before it is sent; leave it at 0 when every second or every step matters.

Or follow the feed with types=offer.created,offer.changed. The loop in Sync a catalogue works unchanged.

Read the event

{
  "id": "evt_034XqBaL2mRLTZPXyRbaUi",
  "type": "offer.changed",
  "spec_version": "1.0",
  "source": "furniops/catalog",
  "subject": "ofr_034XqBaL2mRLTZPXyRbaUg",
  "time": "2026-10-04T10:00:02Z",
  "tenant_id": "org_034XqBaL2mRLTZPXyRbaUj",
  "version": 12,
  "schema_version": 1,
  "correlation_id": "run_034XqBaL2mRLTZPXyRbaUk",
  "causation_id": null,
  "livemode": true,
  "data": {
    "id": "ofr_034XqBaL2mRLTZPXyRbaUg",
    "variant": "var_034XqBaL2mRLTZPXyRbaUf",
    "source": "src_034XqBaL2mRLTZPXyRbaUh",
    "catalogue": "cat_034XqBaL2mRLTZPXyRbaUd",
    "location": "DAL",
    "availability": "in_stock",
    "version": 12,
    "facets": ["price"],
    "fields": [
      {
        "path": "cost",
        "old": 89900,
        "new": 94900,
        "provenance": {
          "source": "src_034XqBaL2mRLTZPXyRbaUh",
          "method": "mapped",
          "confidence": 1,
          "observed_at": "2026-10-04T10:00:00Z"
        }
      }
    ]
  }
}
  • facets says what kind of change it is, so you can skip what you do not need without reading the fields: price (currency and the price columns), stock (quantity, incoming, lead_time_days) and availability. An event can carry up to all three.
  • fields lists each changed field with its old and new value and the provenance of the new value. A price column is an integer in minor units: 94900 with currency usd is $949.00.
  • availability is always present: the offer's availability after the change, whatever changed.
  • location is the source's location code: one variant can have offers at several warehouses.

Handle it

offers.ts
export async function onEvent(event: WebhookEvent): Promise<void> {
  if (event.type !== "offer.changed") return;
  const { data } = event;
  // Apply only newer versions: a late retry of v12 after v13 must change nothing.
  if (data.version <= (await savedVersion(data.id))) return;
  if (!data.facets.includes("price")) return; // a stock or availability change
  for (const field of data.fields) {
    // Each changed field, with its old and new value and where the new value came from.
    console.log(field.path, field.old, "→", field.new, field.provenance?.source);
    if (field.path === "cost") await saveOffer(data.id, data.version, field.new);
  }
}

The version check is what makes this safe. Deliveries are at least once and, across retries, can arrive late: version 12 retried after version 13 arrived must not undo 13.

Which price is which

FieldMeaning
costThe dealer cost: what a dealer pays the vendor. A supplier's bare "price" is read as cost.
msrpThe manufacturer's suggested retail price.
mapThe minimum advertised price: advertising below it can breach the vendor's policy.
priceThe selling price a retailer shows shoppers, when the source states one.
compare_atA "was" price, when the vendor states one.

Which of them a source fills depends on the vendor and, for network data, on your licence: a licence without price sends none of them.

Stock

quantity is units on hand at the offer's location. incoming lists stock on its way with expected dates, and lead_time_days how long the vendor takes to ship. availability is FurniOps' summary of them: in_stock, low_stock, out_of_stock, backorder, preorder, discontinued or unknown.

Check stock at the moment that matters

Events arrive as fast as FurniOps hears of a change, and FurniOps checks a source as often as it learns the source changes (see Freshness). Two calls make it faster where it pays:

  • Record demand for variants you have in carts, just sold or on backorder. Their sources are then checked every 15 minutes until the demand lapses:

    // Variants you sold, have in carts or on backorder are checked at their sources' fastest rate.
    await furniops.demand.record({
      variants: ["var_034XqBaL2mRLTZPXyRbaUc"],
      reason: "cart",
      ttl_minutes: 60,
    });
    // At checkout: ask for a fresh stock check of one variant now.
    const check = await furniops.variants.check("var_034XqBaL2mRLTZPXyRbaUc");
    console.log(check);
  • Ask for a check now at checkout with POST /v1/variants/{id}/checks. Each source offering the variant gets a run, or joins one under way, or answers with one that has just finished. New values arrive as offer.changed events a moment later:

    curl -X POST https://api.furniops.com/v1/variants/var_034XqBaL2mRLTZPXyRbaUf/checks \
      -H "Authorization: Bearer $FURNIOPS_API_KEY" \
      -H "Idempotency-Key: $(uuidgen)"

    It is an expensive operation with a smaller rate budget: use it for the moments that matter, not for every page view.

Suspicious prices are held back

A value far outside a source's history or its peers, such as a $949 sofa suddenly at $9.49, is quarantined instead of published. A vendor's typo does not reach your storefront as an offer.changed.

On this page