Flow · 14 steps
Procure to pay
Buying stock and paying for it: raise a purchase order, authorise it, receive the goods, then settle the supplier invoice at the price actually charged. The interesting part is that these are three services and one story — purchasing owns the order, inventory owns the units, accounting owns what they cost — and no step tells the next what to do beyond issuing its own command.
Services involved
A filled dot is a read; a ring is a call that changes something.
- Inventory Manager:
https://inventory.getmanifold.io - Purchasing Manager:
https://purchasing.getmanifold.io - Accounting Ledger:
https://accounting.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
A SKU has to exist before anything can be ordered against it. Untracked stock may be created with an opening balance; serial- or batch-tracked stock must start at zero and arrive through receiving, so every tracked unit has a document behind it.
POST /items Inventory Manager
{
"sku": "WIDGET-BLUE",
"name": "Blue widget, 40mm",
"quantity": 0
}{
"id": "itm_01a15891-6c3f-7d19-9a3e-7c1b5d8e2f10",
"sku": "WIDGET-BLUE",
"name": "Blue widget, 40mm",
"stock": [],
"createdAt": "2026-08-17T09:00:00.000Z",
"updatedAt": "2026-08-17T09:00:00.000Z"
}Records inventory.item-created.
2 Record what the supplier charges
A supplier's price list is one row per SKU: the unit cost, its currency and the supplier's own code. Setting the same row again appends nothing.
PUT /supplier-prices/cmp_01a15884-6399-7e5f-8a6b-1c2d3e4f5a6b/WIDGET-BLUE Purchasing Manager
{
"unitCostCents": 1250,
"currency": "AUD",
"supplierSku": "NW-40-BLU"
}What the answer holds: the operation's example.
Records purchasing.supplier-price-set.
3 Read the supplier's price list
Every SKU the supplier prices, by SKU; sku narrows to the ones a buyer is ordering.
GET /supplier-prices?supplierId=cmp_01a15884-6399-7e5f-8a6b-1c2d3e4f5a6b Purchasing Manager
What the answer holds: the operation's example.
4 Raise the purchase order
One line per SKU, with the price you expect to pay — here left to the supplier's price list, which costs the line at 12.50. The order is a draft: nothing is committed to inventory and nothing is owed yet.
POST /pos Purchasing Manager
{
"reference": "PO-WIDGET-BLUE",
"supplier": "Northwind Components",
"supplierId": "cmp_01a15884-6399-7e5f-8a6b-1c2d3e4f5a6b",
"lines": [
{
"sku": "WIDGET-BLUE",
"quantity": 40
}
]
}{
"id": "po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d",
"reference": "PO-WIDGET-BLUE",
"supplier": "Northwind Components",
"currency": "AUD",
"number": "PO-00001",
"serial": 1,
"status": "draft",
"lines": [
{
"sku": "WIDGET-BLUE",
"ordered": 40,
"received": 0,
"unitCostCents": 1250
}
],
"createdAt": "2026-08-17T09:00:00.000Z",
"updatedAt": "2026-08-17T09:00:00.000Z"
}From your answer, later steps send id (here po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d) as {poId}.
Records purchasing.po-created.
5 Authorise it
Committing to the purchase. Purchasing tells the inventory service to expect the units, which is what puts them on the SKU's incoming list — so availability can account for stock that is on its way before it lands.
POST /pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/authorise Purchasing Manager
What the answer holds: the operation's example.
Records purchasing.po-authorised, inventory.incoming-expected.
6 Say when it is due
The supplier confirms a delivery day. It is planning, not a change to what was ordered, so it has its own command and fact and leaves inventory's expectations alone — the dashboard measures arriving and overdue orders against it.
PUT /pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/expected Purchasing Manager
{
"expectedOn": "2026-10-01"
}What the answer holds: the operation's example.
Records purchasing.po-expected-changed.
7 Receive the goods
The units physically arrive. Purchasing records the receipt and commands inventory to move the quantity into on-hand, passing the receipt id as the idempotency key — so a retried receipt is acknowledged rather than counted twice.
POST /pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/receipts Purchasing Manager
{
"receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
"lines": [
{
"sku": "WIDGET-BLUE",
"quantity": 40
}
]
}What the answer holds: the operation's example.
Records purchasing.receipt-requested, purchasing.receipt-recorded, inventory.stock-received.
8 Settle the supplier invoice
What the stock actually cost, which is rarely exactly what the order said. Accounting revalues the cost layers; units already sold get a cogs-correction entry rather than a rewritten one, because a posted cost is history.
POST /pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/invoice Purchasing Manager
{
"lines": [
{
"sku": "WIDGET-BLUE",
"unitCostCents": 1310
}
]
}What the answer holds: the operation's example.
Records purchasing.invoice-authorised.
9 Check the stock landed
Forty units on hand, none reserved, and no incoming left outstanding.
GET /items/WIDGET-BLUE Inventory Manager
What the answer holds: the operation's example.
10 Check what it is worth
The SKU's open cost layers and what the stock on hand is valued at. Note what is not here: no ledger entry. Receiving stock costs money but recognises no expense — the cost sits in a layer until the units are sold, which is when a cogs entry posts. Accounting builds this by consuming the purchasing and inventory event streams, so it settles a moment after the receipt commits rather than inside it.
GET /valuation/WIDGET-BLUE Accounting Ledger
What the answer holds: the operation's example.
11 Check the replica is keeping up
The valuation above was folded from local mirrors of the three source journals, fed by logical replication — so the last question is operational: is anything stalled? Each slot reports whether a walsender is attached and how much WAL its source is retaining. This read appends nothing; it never fails, either — a stalled slot is a warning in the payload, not an error, because a source quietly retaining WAL for a dead consumer is exactly the failure that must stay visible while everything else still works.
GET /replica/status Accounting Ledger
What the answer holds: the operation's example.
12 Cut the units' journal at one moment
Where inventory-manager's journal stood at a single instant: the last sequence the database had recorded for the organization by then, by the database's clock. Taken at one at across the three services, the heads are a consistent cut — replaying each with upTo gives the story exactly as it stood. A read: it appends nothing.
GET /events/head?at=2026-11-07T12%3A39%3A42.897Z Inventory Manager
What the answer holds: the operation's example.
13 Cut the order's journal at one moment
Where purchasing-manager's journal stood at a single instant: the last sequence the database had recorded for the organization by then, by the database's clock. Taken at one at across the three services, the heads are a consistent cut — replaying each with upTo gives the story exactly as it stood. A read: it appends nothing.
GET /events/head?at=2026-11-07T12%3A39%3A42.897Z Purchasing Manager
What the answer holds: the operation's example.
14 Cut the ledgers' journal at one moment
Where accounting-manager's journal stood at a single instant: the last sequence the database had recorded for the organization by then, by the database's clock. Taken at one at across the three services, the heads are a consistent cut — replaying each with upTo gives the story exactly as it stood. A read: it appends nothing.
GET /events/head?at=2026-11-07T12%3A39%3A42.897Z Accounting Ledger
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.