StabisoDocs

REST API

Purchase to pay: order, receipt, matching, variance, payment

A company orders goods and services, records what arrives, enters the vendor's bill and pays it. In between, the system compares the bill with the order and with what was received, and refuses to pay a bill that asks for more than was ordered or delivered until a second person has looked at it. This walkthrough runs that cycle on the Swiss entity CH01 with two orders: office chairs (goods, checked against a receipt: a 3-way match) and consulting days (services, checked only against the order: a 2-way match). It is for procurement and accounts-payable integrations, and for the controller who decides about differences.

Before you start

  • Scopes: draft to create and submit orders, record receipts, enter bills and pay them; read for cards and matches; approve to approve an order or a variance; admin to change the tolerances and to issue an API key.
  • Two people. Ordering and approving are different jobs. $ALICE_KEY (scopes read, draft, post, approve, admin) is the buyer and accounts-payable clerk: she orders, records receipts, enters the bills and pays them. $BOB_KEY (read, draft, post, approve) is the controller who approves. An API key counts as the person who created it: Alice and Bob are users who were invited, signed in once and created their key with their own session; in this demo that is already done. $ERP_KEY, the organization's first key, belongs to no person: it can set up and create things, but it can never approve anything. It appears only in refusals.
  • The demo organization: CH01 (Demo AG, CHF) has the active vendor V-BM (Büro Material AG, 30 payment days). September 2026 is open.
  • A purchase order, a receipt and the matching post nothing to the ledger: an order is a commitment, not a liability. The liability is the vendor's bill, posted as usual. What the order controls is paying the bill.

1. Create a purchase order

An order is a draft for an active vendor, numbered at once (PO-2026-000001 and so on, a sequence per entity and year). Each line has a description, a line_type (goods or services), an optional expense or asset account, a quantity, the expected net unit_price and a rate_type (the tax code). requires_receipt says whether bills are matched against what was received (default: yes for goods, no for services). Currency and payment terms default to the vendor's. The call needs an Idempotency-Key.

curl -X POST "$ERP_URL/v1/entities/CH01/purchase-orders" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-order-chairs" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor": "V-BM",
    "order_date": "2026-09-22",
    "expected_date": "2026-09-30",
    "lines": [
      {"description": "Office chairs", "quantity": "10", "unit_price": "100", "account": "4000"}
    ]
  }'
{
  "number": "<any>",
  "status": "draft",
  "vendor": "V-BM",
  "currency": "CHF",
  "payment_terms_days": 30,
  "net_total": "1000.00",
  "approval_id": null,
  "lines": [
    {
      "line_no": 1,
      "description": "Office chairs",
      "line_type": "goods",
      "account": "4000",
      "quantity": "10",
      "unit_price": "100",
      "net_amount": "1000.00",
      "requires_receipt": true,
      "received_quantity": "0",
      "invoiced_quantity": "0",
      "invoiced_amount": "0.00"
    }
  ],
  "receipts": [],
  "bills": []
}

The answer is the order's card: the lines with their ordered, received and invoiced quantities, the receipts and the bills matched to it. It is the same shape every later call returns. The call captured the order number as $ORDER_NO.

2. Read the order, change the draft

GET /v1/purchase-orders/{order} takes the id or the number. The ETag response header names the version; a draft is changed with PATCH and If-Match, so that nobody overwrites a change they have not seen (lines replaces all lines). Only drafts change.

curl "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $ALICE_KEY"
{"status": "draft", "net_total": "1000.00"}
curl -X PATCH "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H 'If-Match: $ETAG' \
  -H "Content-Type: application/json" \
  -d '{"notes": "Deliver to the warehouse, ramp 3"}'
{"status": "draft", "notes": "Deliver to the warehouse, ramp 3"}

3. Submit the order: it waits for a second person

POST /v1/purchase-orders/{order}/submit sends a draft for approval. The default approval policy says that a purchase order is approved by a second person, so the order becomes pending_approval and an approval request with the action approve_purchase_order is opened. The call takes If-Match too. The version read before the change is stale now, and is refused with 412:

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/submit" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H 'If-Match: $ETAG'
{"error": {"code": "precondition_failed"}}

With the version the PATCH returned it works:

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/submit" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H 'If-Match: $ETAG_NEW'
{"status": "pending_approval", "approval_id": "<uuid>"}

The card now carries the pending approval_id, which the call captured. The request is decided with POST /v1/approvals/{id}/approve (or reject; a rejected order goes back to a draft). Three callers are refused, each for its own reason, all with 403. Alice created and submitted the order, and four eyes compare people:

curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment": "approving my own order"}'
{"error": {"code": "four_eyes", "action": "approve"}}

The organization's first key belongs to nobody, and nobody is not a second person:

curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment": "the first key is nobody"}'
{"error": {"code": "forbidden"}}

An agent (an automation) may draft and submit orders but never approves them. Its key cannot even hold the approve scope:

curl -X POST "$ERP_URL/v1/api-keys" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "purchasing bot", "actor_type": "agent", "scopes": ["read", "draft", "post"]}'
{"actor_type": "agent", "scopes": ["read", "draft", "post"]}
curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
  -H "Authorization: Bearer $AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment": "an agent approves"}'
{"error": {"code": "policy_denied", "rule_id": "agents_never_approve"}}

4. A second person approves: the order is open

Bob did not create or submit the order. His approval opens it.

curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
  -H "Authorization: Bearer $BOB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment": "in budget"}'
{"object_type": "purchase_order", "action": "approve_purchase_order", "status": "approved"}
curl "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $BOB_KEY"
{"status": "open", "approval_id": null, "approved_by": {"type": "human"}}

An approved order can no longer be changed (PATCH answers 409). It can receive goods.

5. Record a partial receipt

Six of the ten chairs arrive. POST /v1/purchase-orders/{order}/receipts records what was received per order line: the receipt_date, an optional note (the delivery note number, say) and lines of line_no and quantity. The quantity of a line can be corrected with a negative receipt, but the received total always stays between zero and the ordered quantity. Receipts are append-only and need an approved order and a person (agents are refused). They post nothing. The call needs an Idempotency-Key, answers 201 and returns the order card.

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/receipts" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-receipt-1" \
  -H "Content-Type: application/json" \
  -d '{"receipt_date": "2026-09-23", "note": "Delivery note 4711", "lines": [{"line_no": 1, "quantity": "6"}]}'
{
  "status": "partially_received",
  "received_amount": "600.00",
  "lines": [{"line_no": 1, "quantity": "10", "received_quantity": "6", "invoiced_quantity": "0"}],
  "receipts": [
    {
      "number": "<any>",
      "receipt_date": "2026-09-23",
      "note": "Delivery note 4711",
      "lines": [{"line_no": 1, "description": "Office chairs", "quantity": "6"}]
    }
  ]
}

The order is now partially_received. An order can be cancelled only while nothing was received or invoiced, so cancelling this one is refused:

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/cancel" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-cancel-chairs" \
  -H "Content-Type: application/json" \
  -d '{"reason": "no longer needed"}'
{"error": {"code": "order_status"}}

6. The vendor's bill for all ten: a variance

The vendor bills all ten chairs. The bill is an ordinary purchase bill whose order_reference is the order number (it can come from an e-invoice or from bill capture too: EN 16931 BT-13). When the bill is submitted, the system matches it to the order of that number of the same vendor, pairs each bill line with an order line, and measures the differences:

  • Price variance: the bill line's net amount less the order price times the billed quantity.
  • Quantity variance: what the bill invoices beyond the limit, at the order price. The limit is the quantity received (3-way, for lines that need a receipt) or the quantity ordered (2-way), and what earlier bills of the order invoiced counts first.
  • A variance is allowed up to the lower of tolerance_percent of the line's expected amount and tolerance_amount. By default that is 2% and 5.00 per line; per bill the overcharge may be at most 2% and 20.00. Only unfavourable differences count: a lower price is recorded but never blocks.

Here the bill invoices 10 chairs and only 6 were received: 4 chairs at 100.00 is 400.00 beyond the limit, far above the 5.00 allowed. The outcome is variance. The bill itself posts, because the company owes the money for what the vendor says it delivered, but it is blocked for payment until a second person approves the variance.

curl -X POST "$ERP_URL/v1/documents" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-bill-chairs" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "CH01",
    "kind": "purchase_bill",
    "party": "V-BM",
    "document_date": "2026-09-24",
    "external_number": "BM-6001",
    "order_reference": "$ORDER_NO",
    "lines": [
      {"description": "Office chairs", "quantity": "10", "unit_price": "100", "supply_type": "goods", "account": "4000"}
    ]
  }'
{
  "document": {
    "status": "posted",
    "party_code": "V-BM",
    "external_number": "BM-6001",
    "total": "1081.00",
    "open_amount": "1081.00"
  },
  "submit": {"status": "posted"}
}

GET /v1/documents/{id}/match shows how the bill matched: the outcome, whether payment is blocked, the problems in words, the tolerances in force and, per bill line, the order line it was paired with, how (reference, description, price or single), whether it is 2-way or 3-way and the variances against what is allowed. It answers 404 for a bill that names no order.

curl "$ERP_URL/v1/documents/$BILL_ID/match" \
  -H "Authorization: Bearer $ALICE_KEY"
{
  "document_id": "<uuid>",
  "order_number": "<any>",
  "order_status": "partially_invoiced",
  "status": "variance",
  "payment_block": true,
  "problems": ["<any>", "<any>"],
  "tolerance": {"percent": "2", "amount": "5.00", "bill_amount": "20.00"},
  "price_variance": "0.00",
  "quantity_variance": "400.00",
  "overcharge": "400.00",
  "approval_status": "pending",
  "lines": [
    {
      "document_line_no": 1,
      "description": "Office chairs",
      "quantity": "10",
      "order_line_no": 1,
      "ordered_quantity": "10",
      "received_quantity": "6",
      "paired_by": "description",
      "three_way": true,
      "excess_quantity": "4",
      "price_variance": "0.00",
      "quantity_variance": "400.00",
      "allowed": "5.00",
      "within_tolerance": false
    }
  ]
}

There are two problems: the line (it reads like "10 invoiced, 6 received: 400.00 beyond it (allowed 5.00)") and the bill as a whole (400.00 above the order in total, allowed 20.00). A variance opens an approval request, action approve_match_variance, shown as approval_id and approval_status. The order card now lists the bill with its match status and the block, and the order is partially_invoiced.

curl "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $ALICE_KEY"
{
  "status": "partially_invoiced",
  "invoiced_amount": "1000.00",
  "lines": [{"line_no": 1, "received_quantity": "6", "invoiced_quantity": "10"}],
  "bills": [
    {
      "external_number": "BM-6001",
      "status": "posted",
      "match_status": "variance",
      "payment_block": true
    }
  ]
}

7. The blocked bill cannot be paid

Paying is refused wherever it can happen: a payment (POST /v1/payments), a payment file, a bank line match, and the database itself. A payment proposal leaves the bill out, so a payment run never picks it up. A payment against the bill answers 409:

curl -X POST "$ERP_URL/v1/payments" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-pay-chairs-early" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "CH01",
    "direction": "outgoing",
    "party": "V-BM",
    "payment_date": "2026-09-30",
    "amount": "$BILL_TOTAL",
    "allocations": [{"document": "$BILL_ID", "amount": "$BILL_TOTAL"}]
  }'
{"error": {"code": "bill_payment_blocked"}}

The message names the bill, the order and the way out (POST /v1/documents/{id}/match/approve). The payment proposal for the vendor, which covers everything due until the end of the year, shows nothing to pay:

curl -X POST "$ERP_URL/v1/entities/CH01/payment-proposals" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payment_date": "2026-09-30", "due_until": "2026-12-31", "vendors": ["V-BM"]}'
{"entity": "CH01", "lines": []}

8. The rest of the goods arrives: the block lifts by itself

The remaining four chairs are delivered. Every receipt matches the order's unpaid bills again, so the bill that came before the goods is released without anybody approving anything: it is now within the order, the variance request is withdrawn, and the order has everything received and invoiced.

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/receipts" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-receipt-2" \
  -H "Content-Type: application/json" \
  -d '{"receipt_date": "2026-09-27", "lines": [{"line_no": 1, "quantity": "4"}]}'
{
  "status": "closed",
  "received_amount": "1000.00",
  "invoiced_amount": "1000.00",
  "lines": [{"line_no": 1, "received_quantity": "10", "invoiced_quantity": "10"}],
  "bills": [{"external_number": "BM-6001", "match_status": "matched", "payment_block": false}]
}
curl "$ERP_URL/v1/documents/$BILL_ID/match" \
  -H "Authorization: Bearer $ALICE_KEY"
{
  "status": "matched",
  "payment_block": false,
  "problems": [],
  "overcharge": "0.00",
  "approval_status": "rejected",
  "lines": [{"received_quantity": "10", "excess_quantity": "0", "within_tolerance": true}]
}

approval_status: rejected is the withdrawn request: the system closed it with the comment that the bill matched again without a variance. A receipt can also go the other way: a negative receipt (goods sent back) blocks the order's unpaid bills again, and flags a bill that was already paid for review (review on its match and on the order card, ?review=true on the order list). POST /v1/documents/{id}/match matches a bill again on request, for example a draft before it is submitted; an approved variance stays approved.

9. Pay the bill

The proposal now offers the bill, and the payment goes through.

curl -X POST "$ERP_URL/v1/entities/CH01/payment-proposals" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payment_date": "2026-09-30", "due_until": "2026-12-31", "vendors": ["V-BM"]}'
{
  "lines": [
    {"vendor": "V-BM", "external_number": "BM-6001", "open": "1081.00", "amount": "1081.00", "reason": "due"}
  ]
}
curl -X POST "$ERP_URL/v1/payments" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-pay-chairs" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "CH01",
    "direction": "outgoing",
    "party": "V-BM",
    "payment_date": "2026-09-30",
    "amount": "$BILL_TOTAL",
    "allocations": [{"document": "$BILL_ID", "amount": "$BILL_TOTAL"}]
  }'
{"status": "posted", "submit": {"status": "posted"}}

Releasing the payment as a pain.001 file is covered in From a bill to a payment file.

10. Services and a price variance: 2-way matching

A second order, for five consulting days at 200.00. Services do not need a receipt, so bills are matched against the ordered quantity (2-way). It is created, submitted and approved as before.

curl -X POST "$ERP_URL/v1/entities/CH01/purchase-orders" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-order-consulting" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor": "V-BM",
    "order_date": "2026-09-28",
    "lines": [
      {"description": "Consulting", "line_type": "services", "quantity": "5", "unit_price": "200", "account": "6500"}
    ]
  }'
{
  "status": "draft",
  "net_total": "1000.00",
  "lines": [{"line_type": "services", "requires_receipt": false}]
}
curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER2_NO/submit" \
  -H "Authorization: Bearer $ALICE_KEY"
{"status": "pending_approval", "approval_id": "<uuid>"}
curl -X POST "$ERP_URL/v1/approvals/$APPROVAL2_ID/approve" \
  -H "Authorization: Bearer $BOB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment": "in budget"}'
{"object_type": "purchase_order", "status": "approved"}

The vendor then bills the five days at 230.00 instead of 200.00. The price is 30.00 higher per day, 150.00 more in all, against an allowed 5.00 for the line: another variance. The bill posts and is blocked.

curl -X POST "$ERP_URL/v1/documents" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-bill-consulting" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "CH01",
    "kind": "purchase_bill",
    "party": "V-BM",
    "document_date": "2026-09-29",
    "external_number": "BM-6002",
    "order_reference": "$ORDER2_NO",
    "lines": [
      {"description": "Consulting", "quantity": "5", "unit_price": "230", "account": "6500"}
    ]
  }'
{"document": {"status": "posted", "total": "1243.15"}}
curl "$ERP_URL/v1/documents/$BILL2_ID/match" \
  -H "Authorization: Bearer $ALICE_KEY"
{
  "status": "variance",
  "payment_block": true,
  "price_variance": "150.00",
  "quantity_variance": "0.00",
  "overcharge": "150.00",
  "approval_status": "pending",
  "lines": [
    {
      "paired_by": "description",
      "three_way": false,
      "ordered_quantity": "5",
      "received_quantity": "0",
      "price_variance": "150.00",
      "allowed": "5.00",
      "within_tolerance": false
    }
  ]
}

No receipt can release this one: the variance is a price, not a quantity. Someone must decide that 230.00 is acceptable (the vendor raised its day rate and the buyer agreed) and say so with a reason: POST /v1/documents/{id}/match/approve with {"reason": "..."} (scope approve). It is the same as approving the variance request with POST /v1/approvals/{id}/approve. Four eyes apply: the approver is a known person who did not enter, change, submit or match the bill and did not request the approval; agents never approve. Alice entered the bill and the first key is nobody, so both are refused:

curl -X POST "$ERP_URL/v1/documents/$BILL2_ID/match/approve" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "the day rate was raised"}'
{"error": {"code": "four_eyes", "action": "approve"}}
curl -X POST "$ERP_URL/v1/documents/$BILL2_ID/match/approve" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "the day rate was raised"}'
{"error": {"code": "forbidden"}}

Bob approves with a reason. The reason is kept on the match and in the audit trail. The match keeps its outcome (variance: the bill does differ from the order) but the payment block is gone.

curl -X POST "$ERP_URL/v1/documents/$BILL2_ID/match/approve" \
  -H "Authorization: Bearer $BOB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "day rate raised to 230 as agreed with the vendor on 25 September"}'
{
  "status": "variance",
  "payment_block": false,
  "approval_status": "approved",
  "approved_by": {"type": "human"},
  "approval_reason": "day rate raised to 230 as agreed with the vendor on 25 September"
}

What the approval is bound to: the bill and the order as they were, and at most the overcharge approved (150.00 here). Changing the bill (a new price or quantity on a draft, reopening it) clears the approval, and a later match that finds a larger overcharge asks again; the bill is blocked until a person approves it once more. Now the payment goes through:

curl -X POST "$ERP_URL/v1/payments" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-pay-consulting" \
  -H "Content-Type: application/json" \
  -d '{
    "entity": "CH01",
    "direction": "outgoing",
    "party": "V-BM",
    "payment_date": "2026-09-30",
    "amount": "$BILL2_TOTAL",
    "allocations": [{"document": "$BILL2_ID", "amount": "$BILL2_TOTAL"}]
  }'
{"status": "posted", "submit": {"status": "posted"}}

11. Cards, history, tolerances and cancelling

The first order is closed: everything is received and invoiced. Its card shows what was ordered, received and invoiced per line, the receipts and the bills with their match status.

curl "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $BOB_KEY"
{
  "status": "closed",
  "net_total": "1000.00",
  "received_amount": "1000.00",
  "invoiced_amount": "1000.00",
  "lines": [
    {
      "line_no": 1,
      "quantity": "10",
      "received_quantity": "10",
      "invoiced_quantity": "10",
      "invoiced_amount": "1000.00"
    }
  ],
  "receipts": [
    {"lines": [{"line_no": 1, "quantity": "6"}]},
    {"lines": [{"line_no": 1, "quantity": "4"}]}
  ],
  "bills": [{"external_number": "BM-6001", "match_status": "matched", "payment_block": false}]
}

The order moved through open, partially_received, partially_invoiced and closed as the receipts and bills came in; an order is closed once every line is invoiced and, where it needs a receipt, received. The history lists the audit events of the order, oldest first: creation, changes, submission, approval, each receipt and each status change.

curl "$ERP_URL/v1/purchase-orders/$ORDER_NO/history" \
  -H "Authorization: Bearer $BOB_KEY"
[
  {"action": "purchase_order.create"},
  {"action": "purchase_order.update"},
  {"action": "approval.request"},
  {"action": "approval.approved"},
  {"action": "purchase_order.approve"},
  "..."
]

GET /v1/entities/CH01/purchase-orders lists the orders of an entity, newest first, filtered by status (repeat the parameter), vendor, start and end (order date), q and review:

curl "$ERP_URL/v1/entities/CH01/purchase-orders?status=closed&vendor=V-BM" \
  -H "Authorization: Bearer $BOB_KEY"
[{"status": "closed", "vendor": "V-BM"}, {"status": "closed", "vendor": "V-BM"}]

The tolerances belong to the entity. Until someone sets them, the built-in values apply:

curl "$ERP_URL/v1/entities/CH01/purchase-settings" \
  -H "Authorization: Bearer $BOB_KEY"
{
  "entity": "CH01",
  "default": true,
  "tolerance_percent": "2",
  "tolerance_amount": "5.00",
  "tolerance_bill_amount": "20.00"
}

PUT sets them (scope admin; audited with the previous and the new values). A variance is allowed up to tolerance_percent of a line's expected amount and at most tolerance_amount, the stricter of the two; tolerance_bill_amount limits the total overcharge of a bill. The limits are 10%, 1,000.00 per line and 10,000.00 per bill. Tolerances apply to bills matched from then on.

curl -X PUT "$ERP_URL/v1/entities/CH01/purchase-settings" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tolerance_percent": "3", "tolerance_amount": "10.00", "tolerance_bill_amount": "30.00"}'
{
  "default": false,
  "tolerance_percent": "3",
  "tolerance_amount": "10.00",
  "tolerance_bill_amount": "30.00"
}

An order that has not moved can be cancelled, with a reason that stays on the order (a pending approval request is withdrawn). Idempotency-Key and, optionally, If-Match apply.

curl -X POST "$ERP_URL/v1/entities/CH01/purchase-orders" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-order-spare" \
  -H "Content-Type: application/json" \
  -d '{"vendor": "V-BM", "order_date": "2026-09-30", "lines": [{"description": "Desk lamps", "quantity": "4", "unit_price": "45", "account": "4000"}]}'
{"status": "draft", "net_total": "180.00"}
curl -X POST "$ERP_URL/v1/purchase-orders/$SPARE_NO/cancel" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-cancel-spare" \
  -H "Content-Type: application/json" \
  -d '{"reason": "ordered from another vendor"}'
{"status": "cancelled", "cancel_reason": "ordered from another vendor"}

What can go wrong

Goods cannot be received beyond the ordered quantity (or below zero). The first order already has all ten chairs, so one more is refused with 422:

curl -X POST "$ERP_URL/v1/purchase-orders/$ORDER_NO/receipts" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-receipt-too-many" \
  -H "Content-Type: application/json" \
  -d '{"receipt_date": "2026-09-28", "lines": [{"line_no": 1, "quantity": "1"}]}'
{"error": {"code": "received_more_than_ordered"}}

Only a draft can be changed. The approved order is fixed, and a changed commitment is a new order (or a cancelled one):

curl -X PATCH "$ERP_URL/v1/purchase-orders/$ORDER_NO" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes": "too late"}'
{"error": {"code": "order_status"}}

Tolerances have limits (at most 10% of the line):

curl -X PUT "$ERP_URL/v1/entities/CH01/purchase-settings" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tolerance_percent": "11", "tolerance_amount": "10.00"}'
{}

A cancelled order does not change: cancelling it again is refused with 409.

curl -X POST "$ERP_URL/v1/purchase-orders/$SPARE_NO/cancel" \
  -H "Authorization: Bearer $ALICE_KEY" \
  -H "Idempotency-Key: p2p-cancel-spare-again" \
  -H "Content-Type: application/json" \
  -d '{"reason": "again"}'
{"error": {"code": "order_status"}}

Reference

Stabiso, the Enterprise System of Record · generated from the code and its guides · release 6f03a5e846b3