Flow · 17 steps

Curate a channel catalog

Deciding what one selling view is allowed to see: open a catalog for a channel (an Australian online store, a POS register, a B2B company), include a product in it, price its SKU in the catalog's currency, then read the resolved view the channel consumes. Exposure is opt-in — a product a catalog does not include does not exist for that channel — and a store holding several catalogs merges them, a SKU priced in several taking the lowest price.

Services involved

A filled dot is a read; a ring is a call that changes something.

  • Inventory Manager: https://inventory.getmanifold.io

Every call carries Authorization: Bearer $MANIFOLD_TOKEN unless its step says otherwise (getting a token). Run a flow in your sandbox, not your live organization: see Before you start.

1 Create the item

The SKU master first: catalogs expose products, products wrap SKUs, and a SKU must be an inventory item before the product layer may reference it.

POST /items Inventory Manager

Request · application/json
{
  "sku": "TOTE-CANVAS",
  "name": "Canvas tote",
  "quantity": 0
}

Answers 201. What the answer holds: the operation's example.

Records inventory.item-created.

2 Create the product

The browsable thing over the SKU. One variant here; options, more variants and composite kits behave identically in a catalog.

POST /products Inventory Manager

Request · application/json
{
  "title": "Canvas tote",
  "variants": [
    {
      "sku": "TOTE-CANVAS"
    }
  ]
}

Answers 201. What the answer holds: the operation's example.

From your answer, later steps send id as {productId}.

Records inventory.product-created.

3 Give the product a shape

The option space is re-declarable after creation: axes and values arrive with every variant's new selection (keyed by SKU) in one command, so the product is never caught between shapes. A variant left out keeps its selection, which must still fit.

PUT /products/{productId}/options Inventory Manager

Request · application/json
{
  "options": [
    {
      "name": "Fabric",
      "values": [
        "Canvas"
      ]
    }
  ],
  "variants": [
    {
      "sku": "TOTE-CANVAS",
      "options": [
        {
          "name": "Fabric",
          "value": "Canvas"
        }
      ]
    }
  ]
}

What the answer holds: the operation's example.

Records inventory.product-options-set.

4 Open the AU catalog

A catalog is a channel's whole world: a name the views are assigned by, and the one currency its prices are in. It starts empty — nothing is exposed until a product opts in.

POST /catalogs Inventory Manager

Request · application/json
{
  "name": "AU TOTE-CANVAS",
  "currency": "AUD"
}

Answers 201. What the answer holds: the operation's example.

From your answer, later steps send id as {catalogId} and id as {baseCatalogId}.

Records inventory.catalog-created.

5 Include the product

Exposure is explicit. Including a product exposes all its variants to this catalog's views; including it twice acknowledges idempotently.

PUT /catalogs/{catalogId}/products/{productId} Inventory Manager

What the answer holds: the operation's example.

Records inventory.catalog-product-included.

6 Price the SKU

Per-SKU, in the catalog's currency — composite (kit) SKUs price exactly like stocked ones. The product must already be included: a price without its product would be inert, so it is refused instead.

PUT /catalogs/{catalogId}/prices/TOTE-CANVAS Inventory Manager

Request · application/json
{
  "priceCents": 12300
}

What the answer holds: the operation's example.

Records inventory.catalog-price-set.

7 Read the channel's view

What a channel app's catalog sync, the POS register or the B2B portal actually consumes: included, non-archived products with their variants and prices. A view holding several catalogs merges these, assignment order deciding price conflicts.

GET /catalogs/{catalogId}/view Inventory Manager

What the answer holds: the operation's example.

8 Open a US catalog beside it

A second currency is a second catalog. This one will not be priced by hand at all: its prices are about to be derived from the AU catalog.

POST /catalogs Inventory Manager

Request · application/json
{
  "name": "US TOTE-CANVAS",
  "currency": "USD"
}

Answers 201. What the answer holds: the operation's example.

From your answer, later steps send id as {catalogId}.

Records inventory.catalog-created.

9 Record this hour's forex rate

Forex lives in the platform as facts: one rate per currency pair per hour-long window, recorded by an operator (or a feed driving this same endpoint). An hour with no recording falls back to the newest window before it; re-recording a window is a journalled correction.

PUT /fx/rates Inventory Manager

Request · application/json
{
  "base": "AUD",
  "quote": "USD",
  "rateMicros": 650000,
  "windowStart": "2026-11-07T12:00:00.000Z"
}

What the answer holds: the operation's example.

Records inventory.fx-rate-recorded.

10 Put the US catalog on auto pricing

One fact configures the whole policy: derive every SKU the AU catalog prices at the recorded hourly AUD→USD rate, add 5 %, and round up to the next …9.99 (a base can as well be the SKUs' cost basis, marked up or held to a margin). Derived prices are served marked auto; a price set by hand stands as an override until removed. A channel that prices itself by its own rules (a store's automatic currency conversion) declares channel instead, and manifold derives nothing for it.

PUT /catalogs/{catalogId}/pricing Inventory Manager

Request · application/json
{
  "_tag": "auto",
  "base": {
    "_tag": "catalog",
    "catalogId": "{baseCatalogId}"
  },
  "adjustment": {
    "_tag": "markup",
    "basisPoints": 500
  },
  "rounding": [
    {
      "stepCents": 1000,
      "endingCents": 999
    }
  ]
}

What the answer holds: the operation's example.

Records inventory.catalog-pricing-configured.

11 Read the recorded windows

The platform's forex table, newest first: what every auto-priced catalog is converting with right now.

GET /fx/rates Inventory Manager

What the answer holds: the operation's example.

12 Set the SKU's cost basis

A cost-plus catalog marks up each SKU's replacement cost: the latest receive that carried a cost, or a figure set by hand — a standard cost, or an import's starting figure — in the home currency. Setting the same figure twice appends nothing.

PUT /items/TOTE-CANVAS/cost Inventory Manager

Request · application/json
{
  "costCents": 4000
}

What the answer holds: the operation's example.

Records inventory.sku-cost-set.

13 Read it back

Where the figure came from (set, receipt, or kit for a composite summed from its parts) and when. Not the FIFO average: valuation stays accounting's.

GET /items/TOTE-CANVAS/cost Inventory Manager

What the answer holds: the operation's example.

14 Open a retail catalog priced from cost

A third catalog, this one derived from cost rather than from another catalog.

POST /catalogs Inventory Manager

Request · application/json
{
  "name": "Retail TOTE-CANVAS",
  "currency": "AUD"
}

Answers 201. What the answer holds: the operation's example.

From your answer, later steps send id as {catalogId}.

Records inventory.catalog-created.

15 List the product in it

A cost-plus catalog prices the SKUs it lists that have a cost basis.

PUT /catalogs/{catalogId}/products/{productId} Inventory Manager

What the answer holds: the operation's example.

Records inventory.catalog-product-included.

16 Hold it to a 60 % margin, ending in.99

Cost ÷ (1 − 0.60), rounded up by band: to the next.99 below $100, to the next 9.99 from there. A receipt that changes the cost moves the price on the next read; a price typed by hand overrides.

PUT /catalogs/{catalogId}/pricing Inventory Manager

Request · application/json
{
  "_tag": "auto",
  "base": {
    "_tag": "cost"
  },
  "adjustment": {
    "_tag": "margin",
    "basisPoints": 6000
  },
  "rounding": [
    {
      "fromCents": 0,
      "stepCents": 100,
      "endingCents": 99
    },
    {
      "fromCents": 10000,
      "stepCents": 1000,
      "endingCents": 999
    }
  ]
}

What the answer holds: the operation's example.

Records inventory.catalog-pricing-configured.

17 Read the derived price

40.00 ÷ 0.4 = 100.00, which lands in the second band: 109.99, served marked auto.

GET /catalogs/{catalogId}/prices Inventory Manager

What the answer holds: the operation's example.

What a recorded fact is: Facts and the journal. Why a repeated step changes nothing: Idempotency and retries.