Purchasing Manager API · pos

Record goods arriving against an authorised PO

POST /pos/{poId}/receipts

Record goods arriving against an authorised PO: one delivery, covering one or more of the order's lines (lines, each SKU at most once), as one fact.

Each line captures unit/lot identity when its SKU requires it: serial-tracked SKUs need one serial per unit (409 SerialAlreadyRegistered if any serial was ever received before — purchasing is the uniqueness authority); batch-tracked SKUs need a batchNumber, optionally with expiresAt (409 TrackingMismatch when identity fields don't match the SKU's mode). A pack line is received in packs and books its contents at the pack's cost spread (never serial-tracked). Every line is checked (409 UnknownPoLine, 409 OverReceipt) before anything moves, so a refused receipt moves nothing. An accepted delivery is a durable saga: purchasing appends purchasing.receipt-requested first, then commands the inventory service to receive each line's quantity — locationId names the site the goods landed at (default: the organization's default location); inventory draws the PO's incoming expectation down there first, then wherever else it was expected — and finally appends purchasing.receipt-recorded carrying every line's identity and PO-line cost (with the policy's purchasing.invoice-authorised and purchasing.po-closed in the same transaction when the organization's settings say so), or purchasing.receipt-refused when inventory no longer knows a SKU (404 ItemNotFound). An inventory outage is retried inside the run, which a crash re-drives on boot; a request that outlives the service's wait for the run (30s) answers 503 ReceiptPending while the run continues. receiptId, a caller-minted rcpt_…, is required and is the idempotency key: a retry with the same id is acknowledged from the journal — attaching to a run still in flight, answering from it once done, including once the order has left the receiving states, which the receipt that completed it does itself — and receives nothing twice, and each line's inventory command carries <receiptId>:<sku> so a re-driven line is acknowledged there too (502 InventoryUnavailable is reserved for the acceptance check's SKU lookups). The fold stores the receipt state: the first units in make it partially-received, the receipt completing the last line makes it received. receivedOn is the day the goods came in when that was not today (400 DayInFuture for a day to come) and shipmentId the inbound shipment that brought them (404 UnknownInboundShipment for one the order does not have): the receipt joins the order's receipts, marks the shipment delivered, and is what supplier and carrier performance are measured by. Units that arrived damaged, short, over or wrong are not received here — report them with POST /pos/:poId/issues.

Access

Requires receiving:write.

How scopes come from roles: Scopes and roles. Getting a token: Authentication and app tokens.

Retries

receiptId, a caller-minted rcpt_…, is required and is the idempotency key: a retry with the same id is acknowledged from the journal — attaching to a run still in flight, answering from it once done, including once the order has left the receiving states, which the receipt that completed it does itself — and receives nothing twice, and each line's inventory command carries <receiptId>:<sku> so a re-driven line is acknowledged there too (502 InventoryUnavailable is reserved for the acceptance check's SKU lookups).

A retry with the same key answers 200, with what the first call recorded.

Why every write carries a key: Idempotency and retries.

Facts appended

  • purchasing.receipt-requested
  • purchasing.receipt-recorded
  • purchasing.invoice-authorised
  • purchasing.po-closed
  • purchasing.receipt-refused

What a fact is: Facts and the journal.

AI agent tool

Agents call this as purchasing_record_receipt (Purchasing: Record receipt), in the receiving toolset.

Connecting an agent: AI agents.

Example

curl
curl -X POST 'https://purchasing.getmanifold.io/pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/receipts' \
  -H "Authorization: Bearer $MANIFOLD_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
  "locationId": "loc_01a15856-c554-7a7b-8c9d-0e1f2a3b4c5d",
  "lines": [
    {
      "sku": "WIDGET-BLUE",
      "quantity": 40
    }
  ]
}'
JavaScript (fetch)
const response = await fetch("https://purchasing.getmanifold.io/pos/po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d/receipts", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.MANIFOLD_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
    "locationId": "loc_01a15856-c554-7a7b-8c9d-0e1f2a3b4c5d",
    "lines": [
      {
        "sku": "WIDGET-BLUE",
        "quantity": 40
      }
    ]
  }),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const answer = await response.json();
Request body · application/json
{
  "receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
  "locationId": "loc_01a15856-c554-7a7b-8c9d-0e1f2a3b4c5d",
  "lines": [
    {
      "sku": "WIDGET-BLUE",
      "quantity": 40
    }
  ]
}
Response · 200 · application/json
{
  "id": "po_01a15876-6c9e-7b3a-8c4d-9e0f1a2b3c4d",
  "number": "PO-00001",
  "serial": 1,
  "reference": "PO-WIDGET-BLUE",
  "supplier": "Northwind Components",
  "supplierId": "cmp_01a15884-6399-7e5f-8a6b-1c2d3e4f5a6b",
  "currency": "AUD",
  "status": "received",
  "lines": [
    {
      "sku": "WIDGET-BLUE",
      "ordered": 40,
      "received": 40,
      "cancelled": 0,
      "unitCostCents": 1250
    }
  ],
  "landedCosts": [],
  "payables": [],
  "payments": [],
  "revision": 0,
  "createdAt": "2026-09-22T01:15:00.000Z",
  "updatedAt": "2026-10-01T03:42:00.000Z",
  "expectedOn": "2026-10-01",
  "receipts": [
    {
      "receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
      "receivedOn": "2026-10-01",
      "lines": [
        {
          "sku": "WIDGET-BLUE",
          "quantity": 40
        }
      ]
    }
  ]
}

The panel writes this call in other languages. It does not send it: Manifold's APIs do not take calls from a web page on another site, so run the call from your own code or terminal. The whole API as OpenAPI: purchasing-manager.openapi.json. Base URL https://purchasing.getmanifold.io.

Errors

Every refusal is a status and a JSON body whose _tag names it. A refused call changes nothing.

400

RefusalWhat it meansWhat to do
DayInFuture day, today The request was refused, and nothing changed.Fix the request. Sending it again unchanged gets the same answer.

401

RefusalWhat it meansWhat to do
Unauthorized reason The token is missing, expired or not one Manifold issued.Get a fresh token and send the call again.
TokenAlreadyUsed tokenId A single-use app token was already spent.Mint a new token for this call.

402

RefusalWhat it meansWhat to do
SubscriptionRequired status The organization's subscription has lapsed, so writes are refused.An owner settles billing. Reads still work meanwhile.

403

RefusalWhat it meansWhat to do
TenantRequired userId The token names no organization.Choose the organization, then get a new token.
ScopeRequired required The token's roles do not grant the scope this call needs.Ask an owner or admin for a role that grants it.

404

RefusalWhat it meansWhat to do
PoNotFound poId No such record in this organization.Check the id in the path or the body.
ItemNotFound sku No such record in this organization.Check the id in the path or the body.
UnknownInboundShipment poId, shipmentId No such record in this organization.Check the id in the path or the body.

409

RefusalWhat it meansWhat to do
PoNotAuthorised poId, status The purchase order is not authorised, so it cannot receive or be invoiced.Authorise it first.
UnknownPoLine poId, sku The SKU is not on the purchase order.Add the line first, or check the SKU.
OverReceipt poId, sku, ordered, previouslyReceived, requested More units would arrive than the line ordered, and the organization does not accept over-receipts.Receive at most what is outstanding, or edit the order first.
SerialAlreadyRegistered sku, serials A serial is already in stock. serials lists them.Check the serials and send each one once.
TrackingMismatch sku, tracking, reason The serials or batch do not fit how the SKU is tracked.Name units the way the SKU is tracked.
MirroredFromSource source This area is fed by your source system until the organization goes live.Make the change in the source system, or go live first.

502

RefusalWhat it meansWhat to do
InventoryUnavailable reason The inventory service did not answer, so nothing changed.Send the same request again, with the same key.

503

RefusalWhat it meansWhat to do
ReceiptPending poId, receiptId, reason The receipt is still being recorded. The same request is answered with it once it is saved.Send the same request again, with the same key.
RequestLogUnavailable tokenId, reason The service could not record the request, so it did nothing.Send the same request again, with the same key.

How refusals work everywhere: Errors.

Used in