Flow · 13 steps

Order to cash

Sell stock and get paid: quote, place the order, pick, pack and ship a parcel, take the payment, invoice it and refund part of it.

Each organization chooses which of these steps are manual and which run by themselves. An order keeps the choices made when it was placed, so a later change never alters an order already under way. This walk makes every step manual so you can see each one. With pick and ship set to auto, the same facts are recorded by the one call that places the order.

Run it in a sandbox or a test organization: step 1 changes how every new order is handled. Ids written {orderId} are read from an earlier answer.

Retrying each step. If an answer is lost, send the same request again. The create, payment, invoice and refund carry your key (reference, paymentId, the shipment, refundId) and answer with what the first call recorded. A retried place or ship answers 200 with the order as it stands. A retried pick or pack answers 409 ShipmentNotInState: read the order and check the parcel's shipments[].status — if it is picked or packed, your first call landed.

Services involved

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

  • Fulfillment Manager: https://fulfillment.getmanifold.io
  • Inventory Manager: https://inventory.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 Make every step manual

For a test organization or a sandbox only. This replaces the whole pipeline for every new order: any field left out goes back to its default. To read the pipeline without changing it, use GET /workflow. A step cannot be manual after an automatic one. Payment terms are on-account here, so a parcel may ship before it is paid; under prepaid the ship in step 8 is refused 409 OrderUnpaid until the payment in step 9.

PUT /workflow Fulfillment Manager

Request · application/json
{
  "pick": "manual",
  "pack": "manual",
  "ship": "manual",
  "invoice": "manual",
  "shortfall": "split",
  "paymentTerms": "on-account"
}
Response
{
  "paymentTerms": "on-account",
  "pick": "manual",
  "pack": "manual",
  "ship": "manual",
  "invoice": "manual",
  "shortfall": "split"
}

Records fulfillment.workflow-configured.

2 Have something to sell

Forty units on hand. Stock usually arrives through purchasing (see Procure to pay), but the SKU must exist and hold units before an order can hold any.

POST /items Inventory Manager

Request · application/json
{
  "sku": "WIDGET-BLUE",
  "name": "Blue widget, 40mm",
  "quantity": 40
}

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

Records inventory.item-created.

3 Quote it

A draft quote: one line per item. Leave out unitPriceCents and the line takes its price from a catalog; send it, as here, for a SKU no catalog lists yet, or a quote cannot be issued. Each line gets a lin_ id, which later steps name. Its prices can still change (PUT /orders/{orderId}/lines/{lineId}/price). A quote holds no stock and is not a sale. It names no customer here: an order needs none to ship, be paid or be invoiced. An order made directly (stage: "order", the default) skips this step and the next.

POST /orders Fulfillment Manager

Request · application/json
{
  "reference": "WEB-10231",
  "stage": "draft-quote",
  "lines": [
    {
      "sku": "WIDGET-BLUE",
      "quantity": 6,
      "unitPriceCents": 2400
    }
  ],
  "customer": {
    "personId": "psn_01a15890-1b2c-7d3e-8f4a-5b6c7d8e9f0a"
  }
}

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

From your answer, later steps send id as {orderId} and lines[0].id as {lineId}.

Records fulfillment.quote-created.

4 Issue the quote

Sent to the customer. Its prices are now fixed: a reprice or an edit is refused until the quote is reopened. It still holds no stock.

POST /orders/{orderId}/quote Fulfillment Manager

Request · application/json
{}

What the answer holds: the operation's example.

Records fulfillment.quote-issued.

5 Place it, which holds the stock and opens a parcel

The customer accepted. The order is the quote as it stood, under the same number. Placing it holds the stock and opens a parcel over what it held, so there is a shp_ id to pick against with no further call. POST /orders/{orderId}/shipments opens another parcel, for units held later when the order was short. Placing twice never holds stock twice. Stock is held at the organization's locations by its routing rules; Running more than one location shows how a site is chosen.

POST /orders/{orderId}/place Fulfillment Manager

What the answer holds: the operation's example.

From your answer, later steps send shipments[0].id as {shipmentId}.

Records fulfillment.order-created, fulfillment.stock-allocated, fulfillment.shipment-created.

6 See it on the operations board

The warehouse floor as the ops view shows it: each site's parcels by stage, the longest standing first. The new parcel waits under pending to be picked; nothing is held for payment, refused by a carrier or stuck yet.

GET /orders/board?today=2026-08-17T00%3A00%3A00.000Z&stuckHours=24 Fulfillment Manager

Response
{
  "asOf": "2026-08-17T09:00:00.000Z",
  "today": "2026-08-17T00:00:00.000Z",
  "sites": [
    {
      "locationId": "loc_01a15852-46d1-7a3b-9c0d-1e2f3a4b5c6d",
      "pending": {
        "count": 1,
        "oldest": [
          {
            "orderId": "ord_01a15859-9053-7a55-9e77-0b2c4d6e8f01",
            "orderNumber": "SO-00042",
            "shipmentId": "shp_01a15888-1ced-7bb0-8f21-3a5c7e9d1b04",
            "number": "SH-00042",
            "since": "2026-08-17T09:00:00.000Z"
          }
        ]
      },
      "picked": {
        "count": 0,
        "oldest": []
      },
      "packed": {
        "count": 0,
        "oldest": []
      },
      "shippedToday": 0,
      "shippedYesterday": 0,
      "awaitingPayment": {
        "count": 0,
        "orders": []
      },
      "refusedConsignments": {
        "count": 0,
        "items": []
      },
      "stuck": {
        "count": 0,
        "orders": []
      }
    }
  ],
  "awaitingPayment": {
    "count": 0,
    "orders": []
  },
  "refusedConsignments": {
    "count": 0,
    "items": []
  },
  "stuck": {
    "count": 0,
    "orders": []
  },
  "stuckHours": 24
}

7 Pick it

What the picker took off the shelf. A tracked SKU names its serials or its batch, and those units are taken off the free shelf at once, so no other parcel can claim them. A refused pick records nothing.

POST /orders/{orderId}/shipments/{shipmentId}/pick Fulfillment Manager

Request · application/json
{
  "lines": [
    {
      "lineId": "{lineId}",
      "quantity": 6
    }
  ]
}

What the answer holds: the operation's example.

Records fulfillment.shipment-picked.

8 Pack it

The parcel is closed, with the carton it went in, weighed and measured for the carrier. Stock does not move yet.

POST /orders/{orderId}/shipments/{shipmentId}/pack Fulfillment Manager

Request · application/json
{
  "cartons": [
    {
      "packaging": "carton",
      "weightGrams": 2400,
      "size": {
        "lengthMm": 300,
        "widthMm": 200,
        "heightMm": 150
      }
    }
  ]
}

What the answer holds: the operation's example.

Records fulfillment.shipment-packed.

9 Ship it

Stock leaves here, and its cost becomes the cost of the sale. Each line's held stock is taken from inventory, one fulfillment.dispatched per line. Sending the ship again changes nothing: the parcel ships once.

POST /orders/{orderId}/shipments/{shipmentId}/ship Fulfillment Manager

Request · application/json
{
  "carrier": "DHL",
  "trackingNumber": "JD0002001234567"
}

What the answer holds: the operation's example.

Records fulfillment.dispatched, fulfillment.shipment-shipped.

10 Take the money

The customer pays. Pay the order's money.balanceCents, read from GET /orders/{orderId}: here 6 × 24.00 = 144.00, as this walk's organization has no tax rates. With tax set up, the balance includes it, and paying less leaves the order partially-paid. Paid in full, money goes to paid and the balance to zero. A payment over the balance is refused. You make the pay_ id, so a retry counts the money once. Under prepaid terms, this is what lets a packed parcel ship.

POST /orders/{orderId}/payments Fulfillment Manager

Request · application/json
{
  "paymentId": "pay_01a15863-cdfa-7c8d-9e0f-1a2b3c4d5e6f",
  "amountCents": 14400,
  "method": "card",
  "reference": "ch_3Nq7Xy2eZvKYlo2C1abcdefg"
}

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

Records fulfillment.payment-recorded.

11 Invoice it

A sales invoice for the parcel that shipped. Naming the shipment is what makes a retry safe: it is answered with the invoice already raised. Sent with no shipmentId, the call bills everything shipped and not yet billed, and a retry finds nothing left: 409 NothingToInvoice. What was paid is on the order's money.

POST /orders/{orderId}/invoices Fulfillment Manager

Request · application/json
{
  "shipmentId": "{shipmentId}"
}

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

Records fulfillment.invoice-issued.

12 Refund part of it

Money back. A line marked restock comes back into stock: sellable units go back on the shelf, and their cost comes off the cost of sales. A damaged return is written off at the same cost instead.

POST /orders/{orderId}/refunds Fulfillment Manager

Request · application/json
{
  "refundId": "rfd_01a15887-2e98-7f3a-8c6d-1e2f3a4b5c6d",
  "amountCents": 4800,
  "lines": [
    {
      "lineId": "{lineId}",
      "quantity": 2,
      "restock": true,
      "condition": "sellable"
    }
  ]
}

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

Records fulfillment.refunded.

13 Read the resulting cost of sales

Six units sold and two returned. The ledger shows both entries; the sale is never rewritten.

GET /ledger?sku=WIDGET-BLUE 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.