WinFactor Docs

Configuration Sessions & Price Adjustments

The live pricing write-back — replace the customer-visible breakdown with your own prices in real time

This is the heart of the Deep API: while a customer configures, your system decides what the price breakdown says — line items, names, quantities, unit prices, everything visible where the price is shown.

Lifecycle

customer edits ──► session updated ──► pricing.calculated event ──► your system
     ▲                (configHash A)        (callback URL, token T₁)      │
     │                                                                    │ PUT adjustments
     └──────────── screen updates live ◄───────── adjustment stored ◄─────┘
                                                  (keyed to hash A)

customer edits again ──► configHash B ──► NEW event, token T₂ (T₁ dead)
customer submits     ──► matching adjustment FROZEN onto the submission
org clicks auto-fill ──► fresh pricing.calculated (context "autofill")
  • A configuration session persists the in-progress configuration server-side.
  • Every distinct configuration has a configHash — a canonical fingerprint. Adjustments are keyed to it, so an answer for an old configuration can never paint a wrong price on a new one.
  • The capability token in the callback URL rotates with every event. Use the URL from the event you are answering.

The write-back: PUT adjustments

PUT {data.callback.url}
Content-Type: application/json
{
  "configHash": "sha256:7be1…",
  "lineItems": [
    { "itemKey": "component:k71…:k34…", "description": "Lift-slide door panel (your SKU WW-451)",
      "quantity": 2, "unitPrice": 512.0, "itemType": "door", "sortOffset": 10 },
    { "itemKey": "option:k72…:k88…", "description": "HR++ glass",
      "quantity": 2, "unitPrice": 140.25, "sortOffset": 20 },
    { "description": "Mounting & delivery", "quantity": 1, "unitPrice": 250.0,
      "itemType": "labor", "sortOffset": 90 }
  ]
}
{ "applied": true, "isCurrentConfiguration": true }

The contract is an idempotent full replace:

  • Your lineItems array becomes the entire visible breakdown for that configHash. Retries and duplicate flow runs are harmless — last write wins.
  • Echo and edit: the event's basePricing.lineItems array is directly valid as the lineItems body (extra fields like total are ignored). Reprice one item, drop another, append your own — then send the whole array.
  • The subtotal is always computed server-side from your items. unitPrice must be ≥ 0; quantity > 0; max 200 items.
  • sortOffset orders items; externalRef (per item) is yours to round-trip ERP identifiers.

Stale answers

If the customer changed the configuration while you were computing, you get:

{ "error": { "code": "stale_config_hash", "message": "…",
             "details": { "currentConfigHash": "sha256:91ff…" } } }

Nothing to fix — a newer pricing.calculated event for the current configuration is already on its way to you. Just answer that one.

Timing

  • data.callback.timeoutMs (org-configurable, default 10s) is the customer-facing wait. Inside it, the configurator shows "final pricing is being confirmed"; after it, the base price shows with a neutral note.
  • A late answer still applies — the screen self-heals from the fallback to your pricing the moment it lands.
  • The write window for a token is bounded (several minutes after the event). After that: 403 window_expired.

Reading a session

GET /api/v1/configuration-sessions/{token} (same token, no other auth) returns the live state: current configuration + hash, status (active / submitted / expired), base pricing, your current adjustment, and the submissionId once submitted.

Submission freeze & quote autofill

When the customer submits:

  1. If your adjustment matches the final configuration's hash, it is frozen onto the submission — the session can disappear later without losing your pricing; submission.created carries data.pricing.source: "external" with your items.
  2. If nothing matched, one final pricing.calculated with context: "submission" fires — answer it and the freeze still happens.

On the quote side, auto-fill prefers your frozen items over the base calculation. Every auto-fill click also fires context: "autofill" so you can reprice with current data (indices, stock) right before the quote goes out. The organization user sees an explicit "waiting for external pricing" state and is warned if they try to send while your answer is pending.

Enabling the loop

In Settings → Integrations → Live pricing events: toggle on (this also mints your webhook signing secret), set the timeout, and optionally point a dedicated pricing webhook URL at a separate flow — recommended, so high-frequency pricing events don't crowd your notification flow.

Build it without writing a server: ActivePieces recipe →

Complete examples

Python (10% partner discount on everything, plus a fee):

import requests

def handle_pricing_calculated(event: dict) -> None:
    data = event["data"]
    items = data["basePricing"]["lineItems"]

    for item in items:
        item["unitPrice"] = round(item["unitPrice"] * 0.90, 2)  # your logic here
    items.append({"description": "Configuration service", "quantity": 1,
                  "unitPrice": 75.0, "itemType": "labor", "sortOffset": 99})

    response = requests.put(
        data["callback"]["url"],
        json={"configHash": data["configHash"], "lineItems": items},
        timeout=8,
    )
    response.raise_for_status()
    print(response.json())  # {'applied': True, 'isCurrentConfiguration': True}

Node.js:

export async function handlePricingCalculated(event) {
  const { basePricing, callback, configHash } = event.data;
  const lineItems = basePricing.lineItems.map((item) => ({
    ...item,
    unitPrice: Math.round(item.unitPrice * 0.9 * 100) / 100,
  }));

  const response = await fetch(callback.url, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ configHash, lineItems }),
  });
  if (!response.ok) throw new Error(`adjustment failed: ${response.status}`);
}

curl (echo the base items back unchanged — useful to verify the loop):

jq '{configHash: .data.configHash, lineItems: .data.basePricing.lineItems}' event.json \
  | curl -X PUT "$CALLBACK_URL" -H "Content-Type: application/json" -d @-

On this page