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-requestedpurchasing.receipt-recordedpurchasing.invoice-authorisedpurchasing.po-closedpurchasing.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 -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
}
]
}'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();{
"receiptId": "rcpt_01a15864-bc4f-7f80-b1c3-5d6e7f809a1b",
"locationId": "loc_01a15856-c554-7a7b-8c9d-0e1f2a3b4c5d",
"lines": [
{
"sku": "WIDGET-BLUE",
"quantity": 40
}
]
}{
"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
| Refusal | What it means | What to do |
|---|---|---|
DayInFuture day, today | The request was refused, and nothing changed. | Fix the request. Sending it again unchanged gets the same answer. |
401
| Refusal | What it means | What 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
| Refusal | What it means | What to do |
|---|---|---|
SubscriptionRequired status | The organization's subscription has lapsed, so writes are refused. | An owner settles billing. Reads still work meanwhile. |
403
| Refusal | What it means | What 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
| Refusal | What it means | What 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
| Refusal | What it means | What 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
| Refusal | What it means | What to do |
|---|---|---|
InventoryUnavailable reason | The inventory service did not answer, so nothing changed. | Send the same request again, with the same key. |
503
| Refusal | What it means | What 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
- Procure to pay, step 7: Receive the goods
- Ordering stock in, and counting it when it lands, step 9: Receive against the order, with identity