Purchase-orders
Purchase orders, receipts, matching of bills to orders and the matching tolerances.
GET /v1/documents/{document_id}/match
Get Document Match
How a bill matches the purchase order its order reference names: per line, the order line, price and quantity variances (2-way or 3-way), the tolerance and whether the bill is blocked for payment. 404 when the bill was never matched.
| Required scope | read (admin implies every scope) |
| Operation id | get_document_match_v1_documents__document_id__match_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | document_id |
string | yes | Document id (uuid) or number, e.g. INV-2026-000001. |
Example request
curl "$ERP_URL/v1/documents/INV-2026-000001/match" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MatchOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"document_number": "string",
"order_reference": "string",
"order_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_number": "string",
"order_status": "string",
"status": "matched",
"payment_block": true,
"review": "string",
"problems": [
"string"
],
"problem_reasons": [
{
"words": "string",
"code": "string",
"details": null,
"text": "string"
}
],
"tolerance": {},
"price_variance": "string",
"quantity_variance": "string",
"overcharge": "string",
"matched_by": {},
"matched_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"approval_status": "string",
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_reason": "string",
"held_approved_by": {},
"held_approved_at": "2026-09-21T09:30:00Z",
"lines": [
{
"document_line_no": 1,
"description": "string",
"quantity": "string",
"unit_price": "string",
"order_line_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_line_no": 1,
"order_description": "string",
"order_unit_price": "string",
"ordered_quantity": "string",
"received_quantity": "string",
"paired_by": "string",
"three_way": true,
"excess_quantity": "string",
"price_variance": "string",
"quantity_variance": "string",
"allowed": "string",
"within_tolerance": true,
"similarity": "string",
"runner_up_line_no": 1,
"problem": "string"
}
]
}
POST /v1/documents/{document_id}/match
Match Document
Match a bill to its purchase order again (bills are matched when submitted). An approved variance stays approved. Safe to repeat.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | match_document_v1_documents__document_id__match_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | document_id |
string | yes | Document id (uuid) or number, e.g. INV-2026-000001. |
Example request
curl -X POST "$ERP_URL/v1/documents/INV-2026-000001/match" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MatchOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"document_number": "string",
"order_reference": "string",
"order_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_number": "string",
"order_status": "string",
"status": "matched",
"payment_block": true,
"review": "string",
"problems": [
"string"
],
"problem_reasons": [
{
"words": "string",
"code": "string",
"details": null,
"text": "string"
}
],
"tolerance": {},
"price_variance": "string",
"quantity_variance": "string",
"overcharge": "string",
"matched_by": {},
"matched_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"approval_status": "string",
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_reason": "string",
"held_approved_by": {},
"held_approved_at": "2026-09-21T09:30:00Z",
"lines": [
{
"document_line_no": 1,
"description": "string",
"quantity": "string",
"unit_price": "string",
"order_line_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_line_no": 1,
"order_description": "string",
"order_unit_price": "string",
"ordered_quantity": "string",
"received_quantity": "string",
"paired_by": "string",
"three_way": true,
"excess_quantity": "string",
"price_variance": "string",
"quantity_variance": "string",
"allowed": "string",
"within_tolerance": true,
"similarity": "string",
"runner_up_line_no": 1,
"problem": "string"
}
]
}
POST /v1/documents/{document_id}/match/approve
Approve Document Variance
Approve a bill's purchase order variance with a reason, which releases its payment
block (four eyes: not the person who entered or submitted the bill; agents never).
The same as approving its request with POST /v1/approvals/{id}/approve.
| Required scope | approve (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | approve_document_variance_v1_documents__document_id__match_approve_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | document_id |
string | yes | Document id (uuid) or number, e.g. INV-2026-000001. |
Request body
application/json (required): VarianceApproval
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | Why the variance is fine; required (refused with variance_reason_missing when empty). (max length 500; default "") |
Example request
curl -X POST "$ERP_URL/v1/documents/INV-2026-000001/match/approve" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Price increase agreed by phone with the supplier"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MatchOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"document_number": "string",
"order_reference": "string",
"order_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_number": "string",
"order_status": "string",
"status": "matched",
"payment_block": true,
"review": "string",
"problems": [
"string"
],
"problem_reasons": [
{
"words": "string",
"code": "string",
"details": null,
"text": "string"
}
],
"tolerance": {},
"price_variance": "string",
"quantity_variance": "string",
"overcharge": "string",
"matched_by": {},
"matched_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"approval_status": "string",
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_reason": "string",
"held_approved_by": {},
"held_approved_at": "2026-09-21T09:30:00Z",
"lines": [
{
"document_line_no": 1,
"description": "string",
"quantity": "string",
"unit_price": "string",
"order_line_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"order_line_no": 1,
"order_description": "string",
"order_unit_price": "string",
"ordered_quantity": "string",
"received_quantity": "string",
"paired_by": "string",
"three_way": true,
"excess_quantity": "string",
"price_variance": "string",
"quantity_variance": "string",
"allowed": "string",
"within_tolerance": true,
"similarity": "string",
"runner_up_line_no": 1,
"problem": "string"
}
]
}
GET /v1/entities/{entity}/purchase-orders
List Purchase Orders
Purchase orders of an entity, newest first.
| Required scope | read (admin implies every scope) |
| Operation id | list_purchase_orders_v1_entities__entity__purchase_orders_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
| query | status |
array of "cancelled" | "closed" | "draft" | "open" | "partially_invoiced" | "partially_received" | "pending_approval" or null |
One or more statuses (repeat the parameter). | |
| query | vendor |
string or null | Vendor code or id. | |
| query | start |
string (date) or null | Order date from. | |
| query | end |
string (date) or null | Order date to. | |
| query | q |
string or null | Text in the number, buyer reference or vendor. | |
| query | review |
boolean | Only orders with a paid bill that receipts recorded since would block. (default false) |
|
| query | limit |
integer | How many orders to return, newest first; at most 1000. (min 1; max 1000; default 100) |
Example request
curl "$ERP_URL/v1/entities/CH01/purchase-orders" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of OrderOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1
}
]
POST /v1/entities/{entity}/purchase-orders
Create Purchase Order
Create a draft purchase order for an active vendor (numbered at once). The payment terms and currency default to the vendor's. Nothing is posted to the ledger.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_purchase_order_v1_entities__entity__purchase_orders_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Request body
application/json (required): OrderInput
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string | yes | Vendor code or id; it must be active. |
order_date |
string (date) or null | Date of the order (ISO 8601, e.g. 2026-09-01); default today where the company is. |
|
expected_date |
string (date) or null | Expected delivery; not before the order date. | |
currency |
string or null | Vendor's. (pattern ^[A-Z]{3}$) |
|
buyer_reference |
string or null | Your own reference for the order, e.g. a project or requisition number (up to 100 characters); searchable with q. (max length 100) |
|
payment_terms_days |
integer or null | Vendor's. (min 0; max 365) | |
delivery_address |
DeliveryAddress or null |
Where the goods are delivered; omit for the entity's own address. | |
notes |
string or null | Free text kept on the order (up to 2000 characters). (max length 2000) | |
lines |
array of OrderLineInput |
yes | The order lines, 1 to 500. Each is matched later against the bill's lines. (min items 1; max items 500) |
Example request
curl -X POST "$ERP_URL/v1/entities/CH01/purchase-orders" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"buyer_reference": "REQ-2026-17",
"expected_date": "2026-09-10",
"lines": [
{
"account": "4000",
"description": "Office chairs",
"line_type": "goods",
"quantity": "10",
"unit_price": "100.00"
}
],
"order_date": "2026-09-01",
"vendor": "V-BM"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | OrderCard |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
GET /v1/entities/{entity}/purchase-settings
Get Purchase Settings
The entity's matching tolerances (default 2% and 5.00 per line, the stricter).
| Required scope | read (admin implies every scope) |
| Operation id | get_purchase_settings_v1_entities__entity__purchase_settings_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Example request
curl "$ERP_URL/v1/entities/CH01/purchase-settings" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SettingsOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"tolerance_percent": "string",
"tolerance_amount": "string",
"tolerance_bill_amount": "20.00",
"entity": "string",
"default": true,
"updated_by": {},
"updated_at": "2026-09-21T09:30:00Z"
}
PUT /v1/entities/{entity}/purchase-settings
Set Purchase Settings
Set the matching tolerances: a variance is allowed up to tolerance_percent of a
line's expected amount and at most tolerance_amount (audited).
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | set_purchase_settings_v1_entities__entity__purchase_settings_put |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Request body
application/json (required): SettingsInput
| Field | Type | Required | Description |
|---|---|---|---|
tolerance_percent |
decimal string | yes | Variance allowed as a percent of the expected amount. (min 0; max 10) |
tolerance_amount |
decimal string | yes | Variance allowed per line at most, in the order's currency. (min 0; max 1000) |
tolerance_bill_amount |
decimal string | Overcharge allowed per bill in total at most (the lines' variances added). (min 0; max 10000; default "20.00") |
Example request
curl -X PUT "$ERP_URL/v1/entities/CH01/purchase-settings" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"tolerance_amount": "5.00",
"tolerance_bill_amount": "20.00",
"tolerance_percent": "2"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SettingsOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"tolerance_percent": "string",
"tolerance_amount": "string",
"tolerance_bill_amount": "20.00",
"entity": "string",
"default": true,
"updated_by": {},
"updated_at": "2026-09-21T09:30:00Z"
}
GET /v1/purchase-orders/{order}
Get Purchase Order
A purchase order by id or number: lines with ordered, received and invoiced quantities, receipts and the bills matched to it. The ETag names its version.
| Required scope | read (admin implies every scope) |
| Operation id | get_purchase_order_v1_purchase_orders__order__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
Example request
curl "$ERP_URL/v1/purchase-orders/PO-2026-000001" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrderCard |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
PATCH /v1/purchase-orders/{order}
Update Purchase Order
Change a draft (lines replaces every line); 409 once submitted, 412 when
If-Match names an older version.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| ETag / If-Match | If-Match optional: a changed version answers 412 |
| Response header | ETag names the current version |
| Operation id | update_purchase_order_v1_purchase_orders__order__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
| header | If-Match |
string or null | The ETag of the version you read; a changed purchase order answers 412. (max length 200) |
Request body
application/json (required): OrderUpdate
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string or null | New vendor code or id; cannot be cleared. | |
order_date |
string (date) or null | Date of the order (ISO 8601, e.g. 2026-09-01). Omit to keep the current value. |
|
expected_date |
string (date) or null | Expected delivery date (ISO 8601); not before order_date. Omit when unknown. Omit to keep the current value. |
|
currency |
string or null | ISO 4217 code of the order, e.g. CHF. Default: the vendor's currency. Omit to keep the current value. (pattern ^[A-Z]{3}$) |
|
buyer_reference |
string or null | Your own reference for the order, e.g. a project or requisition number (up to 100 characters); searchable with q. Omit to keep the current value. (max length 100) |
|
payment_terms_days |
integer or null | Payment terms in days, 0 to 365. Default: the vendor's. Omit to keep the current value. (min 0; max 365) | |
delivery_address |
DeliveryAddress or null |
Where the goods are delivered; omit for the entity's own address. Omit to keep the current value. | |
notes |
string or null | Free text kept on the order (up to 2000 characters). Omit to keep the current value. (max length 2000) | |
lines |
array of OrderLineInput or null |
New lines; they replace every line. Omit to keep them. (min items 1; max items 500) |
Example request
curl -X PATCH "$ERP_URL/v1/purchase-orders/PO-2026-000001" \
-H "Authorization: Bearer $ERP_KEY" \
-H 'If-Match: "1"' \
-H "Content-Type: application/json" \
-d '{
"expected_date": "2026-09-12",
"notes": "Deliver to the reception desk."
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrderCard, ETag header |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 412 | If-Match names a version that is no longer current (precondition_failed). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
POST /v1/purchase-orders/{order}/cancel
Cancel Purchase Order
Cancel an order on which nothing was received or invoiced (409 otherwise); a pending approval request is withdrawn. A cancelled order no longer changes.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| ETag / If-Match | If-Match optional: a changed version answers 412 |
| Response header | ETag names the current version |
| Operation id | cancel_purchase_order_v1_purchase_orders__order__cancel_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
| header | If-Match |
string or null | The ETag of the version you read; a changed purchase order answers 412. (max length 200) |
Request body
application/json (required): CancelInput
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why; kept on the order. (min length 1; max length 500) |
Example request
curl -X POST "$ERP_URL/v1/purchase-orders/PO-2026-000001/cancel" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: "1"' \
-H "Content-Type: application/json" \
-d '{
"reason": "Ordered from another supplier"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrderCard, ETag header |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 412 | If-Match names a version that is no longer current (precondition_failed). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
POST /v1/purchase-orders/{order}/close
Close Purchase Order
Close an approved order for the rest (short close) with a reason: what was received
or invoiced stays, the rest of each line is closed (closed_quantity) and the order is
closed. Receipts beyond what is left are refused, and a bill beyond it is a quantity
variance that blocks its payment. 409 for a draft, cancelled or already closed order, or
when nothing is left to close; 412 when If-Match names an older version.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| ETag / If-Match | If-Match optional: a changed version answers 412 |
| Response header | ETag names the current version |
| Operation id | close_purchase_order_v1_purchase_orders__order__close_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
| header | If-Match |
string or null | The ETag of the version you read; a changed purchase order answers 412. (max length 200) |
Request body
application/json (required): CloseInput
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why; kept on the order. (min length 1; max length 500) |
Example request
curl -X POST "$ERP_URL/v1/purchase-orders/PO-2026-000001/close" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: "1"' \
-H "Content-Type: application/json" \
-d '{
"reason": "The supplier cannot deliver the rest"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrderCard, ETag header |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 412 | If-Match names a version that is no longer current (precondition_failed). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
GET /v1/purchase-orders/{order}/history
Purchase Order History
The order's audit events: changes, submission, approval, receipts and status changes, oldest first.
| Required scope | read (admin implies every scope) |
| Operation id | purchase_order_history_v1_purchase_orders__order__history_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
Example request
curl "$ERP_URL/v1/purchase-orders/PO-2026-000001/history" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of AuditEventOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": 1,
"at": "2026-09-21T09:30:00Z",
"actor": {},
"action": "string",
"object_type": "string",
"object_id": "string",
"request_id": "string",
"idempotency_key": "string",
"data": {}
}
]
POST /v1/purchase-orders/{order}/receipts
Receive Purchase Order
Record goods or services received per order line (a negative quantity corrects an earlier receipt). A line of a stock item, or any other line that needs a receipt and names an account, posts at receipt value against the GR/IR account (see goods received). Bills of the order blocked by a quantity variance are matched again once the receipt is posted, so a bill that arrived before the goods is released.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | receive_purchase_order_v1_purchase_orders__order__receipts_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
Request body
application/json (required): ReceiptInput
| Field | Type | Required | Description |
|---|---|---|---|
receipt_date |
string (date) or null | Date the goods or services arrived (ISO 8601); default today where the company is. | |
note |
string or null | Free text, e.g. the delivery note number (up to 500 characters). (max length 500) | |
warehouse |
string or null | Code or id of the warehouse the stock items come into (or go back from); default the entity's default warehouse. Lines that are no stock items ignore it. (max length 40) | |
lines |
array of ReceiptLineInput |
yes | What was received per order line, 1 to 500 lines. (min items 1; max items 500) |
Example request
curl -X POST "$ERP_URL/v1/purchase-orders/PO-2026-000001/receipts" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"lines": [
{
"line_no": 1,
"quantity": "6"
}
],
"note": "Delivery note 4711",
"receipt_date": "2026-09-08"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | OrderCard |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
POST /v1/purchase-orders/{order}/submit
Submit Purchase Order
Submit a draft for approval. By default a second person approves it
(POST /v1/approvals/{id}/approve, action approve_purchase_order); when the approval
policy allows it, a person with the approve scope approves it at once. Agents may
submit, never approve.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| ETag / If-Match | If-Match optional: a changed version answers 412 |
| Response header | ETag names the current version |
| Operation id | submit_purchase_order_v1_purchase_orders__order__submit_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | order |
string | yes | Purchase order id (uuid) or number, e.g. PO-2026-000001. |
| header | If-Match |
string or null | The ETag of the version you read; a changed purchase order answers 412. (max length 200) |
Example request
curl -X POST "$ERP_URL/v1/purchase-orders/PO-2026-000001/submit" \
-H "Authorization: Bearer $ERP_KEY" \
-H 'If-Match: "1"'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrderCard, ETag header |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 412 | If-Match names a version that is no longer current (precondition_failed). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"vendor": "string",
"vendor_name": "string",
"vendor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"currency": "string",
"order_date": "2026-09-21",
"expected_date": "2026-09-21",
"buyer_reference": "string",
"net_total": "string",
"created_at": "2026-09-21T09:30:00Z",
"version": 1,
"payment_terms_days": 1,
"delivery_address": {},
"notes": "string",
"cancel_reason": "string",
"close_reason": "string",
"closed_by": {},
"closed_at": "2026-09-21T09:30:00Z",
"created_by": {},
"approved_by": {},
"approved_at": "2026-09-21T09:30:00Z",
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"refusal": {
"decided_by": {},
"decided_at": "2026-09-21T09:30:00Z",
"comment": "string"
},
"received_amount": "string",
"invoiced_amount": "string",
"lines": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"line_no": 1,
"description": "string",
"line_type": "string",
"account": "string",
"quantity": "string",
"unit_price": "string",
"net_amount": "string",
"rate_type": "string",
"tax_category": "string",
"requires_receipt": true,
"received_quantity": "string",
"invoiced_quantity": "string",
"invoiced_amount": "string",
"closed_quantity": "0",
"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"item": "string"
}
],
"receipts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"receipt_date": "2026-09-21",
"note": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"lines": [
null
]
}
],
"bills": [
{
"document_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"external_number": "string",
"status": "string",
"document_date": "2026-09-21",
"currency": "string",
"total": "string",
"match_status": "string",
"payment_block": true,
"review": "string"
}
]
}
Schemas
CancelInput
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why; kept on the order. (min length 1; max length 500) |
CloseInput
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why; kept on the order. (min length 1; max length 500) |
DeliveryAddress
| Field | Type | Required | Description |
|---|---|---|---|
name |
string or null | Name of the recipient at the address (up to 70 characters). (max length 70) | |
street |
string or null | Street (up to 70 characters). (max length 70) | |
building_number |
string or null | Building number (up to 16 characters). (max length 16) | |
postal_code |
string or null | Postal code (up to 16 characters). (max length 16) | |
town |
string or null | Town (up to 35 characters). (max length 35) | |
country |
string or null | ISO 3166-1 alpha-2 country code in capitals, e.g. CH. (pattern ^[A-Z]{2}$) |
MatchLineOut
| Field | Type | Required | Description |
|---|---|---|---|
document_line_no |
integer | yes | Number of the bill line. |
description |
string | yes | Description on the bill line. |
quantity |
string | yes | Quantity billed. |
unit_price |
string | yes | Unit price billed. |
order_line_id |
string (uuid) or null | yes | Id of the order line paired with it; null when the line is not on the order. |
order_line_no |
integer or null | yes | Number of that order line. |
order_description |
string or null | yes | Description of that order line. |
order_unit_price |
string or null | yes | Unit price on that order line. |
ordered_quantity |
string or null | yes | Quantity ordered on that line. |
received_quantity |
string or null | yes | Received on the order line now, on posted receipts. |
paired_by |
string | yes | reference, description, price, single or none. |
three_way |
boolean | yes | The order line needs a receipt (3-way match). |
excess_quantity |
string | yes | Quantity billed beyond what was ordered (2-way) or received (3-way). |
price_variance |
string | yes | Amount by which the price exceeds the order's, a decimal string. |
quantity_variance |
string | yes | Amount of the excess quantity at the order price, a decimal string. |
allowed |
string | yes | The variance the tolerances allow on the line. |
within_tolerance |
boolean | yes | Whether the line's variance is within the tolerances. |
similarity |
string or null | How alike the descriptions are, 0 to 1 (pairing evidence). | |
runner_up_line_no |
integer or null | The most similar other order line, if any. | |
problem |
string or null | yes | Why the line does not match; null when it does. |
MatchOut
| Field | Type | Required | Description |
|---|---|---|---|
document_id |
string (uuid) | yes | Id of the bill. |
document_number |
string or null | yes | Number of the bill; null before it is posted. |
order_reference |
string | yes | The order number the bill names (order_reference). |
order_id |
string (uuid) or null | yes | Id of the purchase order found; null when none. |
order_number |
string or null | yes | Number of that order. |
order_status |
string or null | yes | Status of that order. |
status |
"matched" | "unmatched" | "variance" |
yes | matched, variance (payment is blocked until approved) or unmatched (the bill quotes an order that is none of its vendor's in its company: its posting waits for a second person). |
payment_block |
boolean | yes | A variance nobody approved: the bill is not paid. |
review |
string or null | A paid bill that receipts recorded since would block: why. | |
problems |
array of string | yes | What is wrong, one sentence each; empty when matched. |
problem_reasons |
array of Reason or null |
problems by code (purchaseMatch.problem) and the values they name, in the same order, for the screens to word; null for a match stored before they were kept. |
|
tolerance |
object | yes | The tolerances applied: percent, amount and bill_amount. |
price_variance |
string | yes | Total price variance of the lines, a decimal string. |
quantity_variance |
string | yes | Total quantity variance of the lines, a decimal string. |
overcharge |
string | yes | Higher prices, larger quantities, lines not ordered. |
matched_by |
object | yes | Who matched the bill (type, id, name). |
matched_at |
string (date-time) | yes | When it was matched (UTC). |
approval_id |
string (uuid) or null | yes | Id of the approval request for the variance; null when none. |
approval_status |
string or null | yes | pending, approved or rejected; null when none. |
approved_by |
object or null | yes | Who approved the variance; null while not approved. |
approved_at |
string (date-time) or null | yes | When it was approved (UTC). |
approval_reason |
string or null | yes | The reason given with the approval. |
held_approved_by |
object or null | Of an unmatched bill: the second person who approved its posting, on the approval request that held it because of the unknown order (type, id, name); null while none has (not decided yet, or a bill posted before such bills were held). |
|
held_approved_at |
string (date-time) or null | When that approval was given (UTC); null when none. | |
lines |
array of MatchLineOut |
yes | The match per bill line. |
OrderBillOut
A bill matched to the order and how it matched.
| Field | Type | Required | Description |
|---|---|---|---|
document_id |
string (uuid) | yes | Id of the bill. |
number |
string or null | yes | Number of the bill; null before it is posted. |
external_number |
string or null | yes | The vendor's own invoice number. |
status |
string | yes | Status of the bill (draft, pending_approval, posted, rejected, void). |
document_date |
string (date) | yes | Date of the bill. |
currency |
string | yes | Currency of the bill. |
total |
string | yes | Gross total of the bill, a decimal string. |
match_status |
string | yes | matched, variance or unmatched. |
payment_block |
boolean | yes | True while an unapproved variance blocks payment of the bill. |
review |
string or null | Paid, but receipts now disagree: why. |
OrderCard
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the purchase order. |
number |
string | yes | Order number, e.g. PO-2026-000001, from a sequence per entity and year. |
status |
string | yes | draft, pending_approval, open, partially_received, partially_invoiced, closed (received and invoiced, or closed for the rest) or cancelled. |
vendor |
string | yes | Vendor code. |
vendor_name |
string | yes | Name of the vendor. |
vendor_id |
string (uuid) | yes | Id of the vendor. |
currency |
string | yes | ISO 4217 currency of the order. |
order_date |
string (date) | yes | Date of the order. |
expected_date |
string (date) or null | yes | Expected delivery date; null when none. |
buyer_reference |
string or null | yes | Your own reference for the order; null when none. |
net_total |
string | yes | Expected net total of all lines, a decimal string. |
created_at |
string (date-time) | yes | When the order was created (UTC). |
version |
integer | yes | Raised on every change; the ETag names it. |
payment_terms_days |
integer | yes | Payment terms in days. |
delivery_address |
object or null | yes | Delivery address; null when none. |
notes |
string or null | yes | Free text on the order. |
cancel_reason |
string or null | yes | Why the order was cancelled; null otherwise. |
close_reason |
string or null | Why the order was closed for the rest (short close); null otherwise. | |
closed_by |
object or null | Who closed the order for the rest; null otherwise. | |
closed_at |
string (date-time) or null | When it was closed for the rest (UTC); null otherwise. | |
created_by |
object | yes | Who created the order (type, id, name). |
approved_by |
object or null | yes | Who approved the order; null until approved. |
approved_at |
string (date-time) or null | yes | When it was approved (UTC). |
approval_id |
string (uuid) or null | yes | The pending approval request, if any. |
refusal |
OrderRefusal or null |
A draft whose last approval request was refused: who refused it, when and why. null otherwise (also once it is submitted again). | |
received_amount |
string | yes | Received quantities at the order prices. |
invoiced_amount |
string | yes | Net amount invoiced so far on the bills of the order, less what posted credit notes of those bills credited. |
lines |
array of OrderLineOut |
yes | The lines with ordered, received and invoiced quantities. |
receipts |
array of ReceiptOut |
yes | The receipts, oldest first. |
bills |
array of OrderBillOut |
yes |
OrderInput
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string | yes | Vendor code or id; it must be active. |
order_date |
string (date) or null | Date of the order (ISO 8601, e.g. 2026-09-01); default today where the company is. |
|
expected_date |
string (date) or null | Expected delivery; not before the order date. | |
currency |
string or null | Vendor's. (pattern ^[A-Z]{3}$) |
|
buyer_reference |
string or null | Your own reference for the order, e.g. a project or requisition number (up to 100 characters); searchable with q. (max length 100) |
|
payment_terms_days |
integer or null | Vendor's. (min 0; max 365) | |
delivery_address |
DeliveryAddress or null |
Where the goods are delivered; omit for the entity's own address. | |
notes |
string or null | Free text kept on the order (up to 2000 characters). (max length 2000) | |
lines |
array of OrderLineInput |
yes | The order lines, 1 to 500. Each is matched later against the bill's lines. (min items 1; max items 500) |
OrderLineInput
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | yes | What is ordered (1 to 500 characters); a bill line is paired with the order line by this text when it names no order line. (min length 1; max length 500) |
line_type |
"goods" | "services" |
goods (default) or services. (default "goods") |
|
account |
string or null | Expense or asset account code. | |
quantity |
decimal string | yes | Ordered quantity, a decimal string above 0 with at most 6 decimals, e.g. 10. (greater than 0) |
unit_price |
decimal string | yes | Expected net unit price. (min 0) |
rate_type |
"exempt" | "parking" | "reduced" | "special" | "standard" | "super_reduced" | "zero" |
The tax code of the line. (default "standard") |
|
tax_category |
string or null | One of the categories of the rate (GET /v1/entities/{entity}/vat-rate-types), e.g. books: it chooses the rate where the rate type has several (France reduced: 5.5 % or 10 %) and names the kind of an exempt supply (insurance). Omit for none. (max length 200) |
|
requires_receipt |
boolean or null | Bills are matched 3-way against received quantities (default: goods yes, services no); otherwise 2-way against the ordered quantity. | |
item |
string or null | A stock item of the order's entity the line buys (its product's code or id); the quantity is in the item's unit. Received goods then enter stock at the line's net price. Such a line is goods and needs a receipt. (max length 100) |
OrderLineOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the order line. |
line_no |
integer | yes | Line number, from 1. |
description |
string | yes | What is ordered. |
line_type |
string | yes | goods or services. |
account |
string or null | yes | Account code. |
quantity |
string | yes | Ordered quantity, a decimal string. |
unit_price |
string | yes | Expected net unit price, a decimal string. |
net_amount |
string | yes | Expected net amount. |
rate_type |
string | yes | The tax code. |
tax_category |
string or null | yes | Named tax category; null when the rate type decides. |
requires_receipt |
boolean | yes | 3-way match against received quantities. |
received_quantity |
string | yes | Quantity received so far, from the receipts (not those rejected). |
invoiced_quantity |
string | yes | Quantity on the bills matched to the order (bills that count), less what posted credit notes of those bills credited of the line (credit note lines that name the bill line, credits_line). |
invoiced_amount |
string | yes | Net amount on those bills less those credit notes, a decimal string. |
closed_quantity |
string | Quantity closed when the order was closed for the rest: no longer expected, neither received nor invoiced. (default "0") |
|
product_id |
string (uuid) or null | The stock item the line buys (its product's id): received goods enter stock; null for any other line. | |
item |
string or null | Code of that stock item's product; null for any other line. |
OrderOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the purchase order. |
number |
string | yes | Order number, e.g. PO-2026-000001, from a sequence per entity and year. |
status |
string | yes | draft, pending_approval, open, partially_received, partially_invoiced, closed (received and invoiced, or closed for the rest) or cancelled. |
vendor |
string | yes | Vendor code. |
vendor_name |
string | yes | Name of the vendor. |
vendor_id |
string (uuid) | yes | Id of the vendor. |
currency |
string | yes | ISO 4217 currency of the order. |
order_date |
string (date) | yes | Date of the order. |
expected_date |
string (date) or null | yes | Expected delivery date; null when none. |
buyer_reference |
string or null | yes | Your own reference for the order; null when none. |
net_total |
string | yes | Expected net total of all lines, a decimal string. |
created_at |
string (date-time) | yes | When the order was created (UTC). |
version |
integer | yes | Raised on every change; the ETag names it. |
OrderRefusal
The approval a second person refused, which returned the order to draft.
| Field | Type | Required | Description |
|---|---|---|---|
decided_by |
object | yes | Who refused it (type, id, name). |
decided_at |
string (date-time) | yes | When it was refused (UTC). |
comment |
string or null | yes | The reason given with the refusal; null when none. |
OrderUpdate
Changes to a draft; omitted fields keep their value, lines replaces every line.
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string or null | New vendor code or id; cannot be cleared. | |
order_date |
string (date) or null | Date of the order (ISO 8601, e.g. 2026-09-01). Omit to keep the current value. |
|
expected_date |
string (date) or null | Expected delivery date (ISO 8601); not before order_date. Omit when unknown. Omit to keep the current value. |
|
currency |
string or null | ISO 4217 code of the order, e.g. CHF. Default: the vendor's currency. Omit to keep the current value. (pattern ^[A-Z]{3}$) |
|
buyer_reference |
string or null | Your own reference for the order, e.g. a project or requisition number (up to 100 characters); searchable with q. Omit to keep the current value. (max length 100) |
|
payment_terms_days |
integer or null | Payment terms in days, 0 to 365. Default: the vendor's. Omit to keep the current value. (min 0; max 365) | |
delivery_address |
DeliveryAddress or null |
Where the goods are delivered; omit for the entity's own address. Omit to keep the current value. | |
notes |
string or null | Free text kept on the order (up to 2000 characters). Omit to keep the current value. (max length 2000) | |
lines |
array of OrderLineInput or null |
New lines; they replace every line. Omit to keep them. (min items 1; max items 500) |
ReceiptLineOut
| Field | Type | Required | Description |
|---|---|---|---|
line_no |
integer | yes | Number of the order line. |
description |
string | yes | Description of the order line. |
quantity |
string | yes | Quantity received (negative for a correction). |
ReceiptOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the receipt. |
number |
string | yes | Receipt number. |
status |
string | yes | posted; pending_approval while its posting waits for a second person (not counted by the 3-way match yet); rejected when the second person rejected it: it no longer counts as received. |
receipt_date |
string (date) | yes | Date the goods or services arrived. |
note |
string or null | yes | Free text, e.g. the delivery note number; null when none. |
created_by |
object | yes | Who recorded it (type, id, name). |
created_at |
string (date-time) | yes | When it was recorded (UTC). |
lines |
array of ReceiptLineOut |
yes | What was received per order line. |
SettingsInput
| Field | Type | Required | Description |
|---|---|---|---|
tolerance_percent |
decimal string | yes | Variance allowed as a percent of the expected amount. (min 0; max 10) |
tolerance_amount |
decimal string | yes | Variance allowed per line at most, in the order's currency. (min 0; max 1000) |
tolerance_bill_amount |
decimal string | Overcharge allowed per bill in total at most (the lines' variances added). (min 0; max 10000; default "20.00") |
SettingsOut
| Field | Type | Required | Description |
|---|---|---|---|
tolerance_percent |
string | yes | Variance allowed as a percent of the expected amount. |
tolerance_amount |
string | yes | Variance allowed per line at most, in the order's currency. |
tolerance_bill_amount |
string | Overcharge allowed per bill in total at most (the lines' variances added). (default "20.00") |
|
entity |
string | yes | Entity code the tolerances belong to. |
default |
boolean | yes | True while the entity uses the built-in tolerances. |
updated_by |
object or null | Who set them last; null while the built-in defaults apply. | |
updated_at |
string (date-time) or null | When (UTC). |
VarianceApproval
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | Why the variance is fine; required (refused with variance_reason_missing when empty). (max length 500; default "") |