---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Operations
GET /purchase-orders
List ERP purchase orders (procurement commitments to vendors)
POs the company has raised against a vendor, with totalAmount, currency, lineItems, approver, expectedDelivery and status draft → pending_approval → approved → sent → partially_received → received (or cancelled). Vendor bills owed against these POs are invoices; employee out-of-pocket spend lives in the expense app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ERP purchase orders (procurement commitments to vendors)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PurchaseOrder"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "POs the company has raised against a vendor, with totalAmount, currency, lineItems, approver, expectedDelivery and status draft → pending_approval → approved → sent → partially_received → received (or cancelled). Vendor bills owed against these POs are invoices; employee out-of-pocket spend lives in the expense app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key)."
}
POST /purchase-orders
Create ERP purchase order (raise a new PO to a vendor)
Raises a new procurement commitment against a vendor, normally in draft or pending_approval. Use invoices for a bill you have already received, and the verify-match tool to reconcile a PO against its invoice and goods receipt.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create ERP purchase order (raise a new PO to a vendor)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PurchaseOrder"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PurchaseOrder"
}
}
}
}
},
"description": "Raises a new procurement commitment against a vendor, normally in draft or pending_approval. Use invoices for a bill you have already received, and the verify-match tool to reconcile a PO against its invoice and goods receipt."
}
GET /purchase-orders/{id}
Get purchase order
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get purchase order",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PurchaseOrder"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /purchase-orders/{id}
Update or transition an ERP purchase order
Partial update, and the way to move PO status through its lifecycle. State-machine rules are enforced: a cancelled PO cannot be modified at all, a fully 'received' PO cannot be cancelled, and only draft/pending_approval POs can be approved.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update or transition an ERP purchase order",
"description": "Partial update, and the way to move PO status through its lifecycle. State-machine rules are enforced: a cancelled PO cannot be modified at all, a fully 'received' PO cannot be cancelled, and only draft/pending_approval POs can be approved.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PurchaseOrder"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PurchaseOrder"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /purchase-orders/{id}
Delete purchase order
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete purchase order",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /purchase-orders/{id}/verify-match
3-way match verification
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "3-way match verification",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invoice_id": {
"type": "string",
"example": "INV-0001"
},
"receipt_confirmed": {
"type": "boolean",
"example": true
}
},
"required": [
"invoice_id",
"receipt_confirmed"
]
}
}
}
},
"responses": {
"200": {
"description": "Match result",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"matched": {
"type": "boolean"
},
"po_id": {
"type": "string"
},
"invoice_id": {
"type": "string"
},
"status": {
"type": "string"
}
}
}
}
}
},
"404": {
"description": "Not found"
},
"422": {
"description": "Match validation failed"
}
}
}
GET /invoices
List ERP vendor invoices (accounts payable)
Bills the company owes suppliers, with vendor, amount, currency, dueDate, an optional poId link for 3-way match, and status draft → pending_payment → paid (or void / overdue). These are supplier bills — employee expense reports live in the expense app and customer billing is not modelled here. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ERP vendor invoices (accounts payable)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Invoice"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "Bills the company owes suppliers, with vendor, amount, currency, dueDate, an optional poId link for 3-way match, and status draft → pending_payment → paid (or void / overdue). These are supplier bills — employee expense reports live in the expense app and customer billing is not modelled here. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key)."
}
POST /invoices
Create ERP vendor invoice (record a bill in accounts payable)
Records a bill received from a vendor. Set poId to link it to the originating purchase order so the PO's verify-match tool can reconcile PO, invoice and receipt.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create ERP vendor invoice (record a bill in accounts payable)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
}
},
"description": "Records a bill received from a vendor. Set poId to link it to the originating purchase order so the PO's verify-match tool can reconcile PO, invoice and receipt."
}
GET /invoices/{id}
Get invoice
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get invoice",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /invoices/{id}
Update or transition an ERP vendor invoice
Partial update, and the way to move invoice status (e.g. pending_payment → paid). State-machine rules are enforced: a paid invoice cannot be voided (issue a credit memo or a reversal journal entry instead), a voided invoice cannot be marked paid, and a voided invoice is read-only apart from notes.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update or transition an ERP vendor invoice",
"description": "Partial update, and the way to move invoice status (e.g. pending_payment → paid). State-machine rules are enforced: a paid invoice cannot be voided (issue a credit memo or a reversal journal entry instead), a voided invoice cannot be marked paid, and a voided invoice is read-only apart from notes.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /invoices/{id}
Delete invoice
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete invoice",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /inventory
List ERP inventory items (stocked goods by SKU)
On-hand stock records — itemName, sku, quantity, reorderPoint, unitCost and warehouseLocation. Use it for stock-on-hand and reorder questions; capitalised equipment is tracked in assets, buying more stock goes through purchase-orders, and live device telemetry lives in the dex app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ERP inventory items (stocked goods by SKU)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InventoryItem"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "On-hand stock records — itemName, sku, quantity, reorderPoint, unitCost and warehouseLocation. Use it for stock-on-hand and reorder questions; capitalised equipment is tracked in assets, buying more stock goes through purchase-orders, and live device telemetry lives in the dex app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key)."
}
POST /inventory
Create ERP inventory item (stock a new SKU)
Adds a stocked SKU with its opening quantity, reorder point, unit cost and warehouse location. For a capitalised piece of equipment create an asset instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create ERP inventory item (stock a new SKU)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InventoryItem"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InventoryItem"
}
}
}
}
},
"description": "Adds a stocked SKU with its opening quantity, reorder point, unit cost and warehouse location. For a capitalised piece of equipment create an asset instead."
}
GET /inventory/{id}
Get inventory item
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get inventory item",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InventoryItem"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /inventory/{id}
Update inventory item
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update inventory item",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InventoryItem"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InventoryItem"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /inventory/{id}
Delete inventory item
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete inventory item",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /journal-entries
List ERP general-ledger journal entries
Double-entry accounting records — type (debit/credit/adjustment/accrual/reversal), debitAccount, creditAccount, amount, accounting period and status draft → posted → reversed, plus reversalOf for entries created by a reversal. This is the GL; vendor bills are invoices and procurement commitments are purchase-orders. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ERP general-ledger journal entries",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/JournalEntry"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "Double-entry accounting records — type (debit/credit/adjustment/accrual/reversal), debitAccount, creditAccount, amount, accounting period and status draft → posted → reversed, plus reversalOf for entries created by a reversal. This is the GL; vendor bills are invoices and procurement commitments are purchase-orders. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key)."
}
POST /journal-entries
Create ERP journal entry (book a GL debit/credit)
Books a new general-ledger entry for a period, normally as a draft that is later posted. To undo an already-posted entry use the reverse tool rather than editing or deleting the original.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create ERP journal entry (book a GL debit/credit)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
}
},
"description": "Books a new general-ledger entry for a period, normally as a draft that is later posted. To undo an already-posted entry use the reverse tool rather than editing or deleting the original."
}
GET /journal-entries/{id}
Get journal entry
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get journal entry",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /journal-entries/{id}
Update or post an ERP journal entry
Partial update, most commonly setting status to 'posted'. State-machine rules are enforced: reversed entries are read-only, and on a posted entry the financial fields (amount, debitAccount, creditAccount) cannot be changed — create a reversal instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update or post an ERP journal entry",
"description": "Partial update, most commonly setting status to 'posted'. State-machine rules are enforced: reversed entries are read-only, and on a posted entry the financial fields (amount, debitAccount, creditAccount) cannot be changed — create a reversal instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /journal-entries/{id}
Delete journal entry
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete journal entry",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /journal-entries/{id}/reverse
Reverse a journal entry
Marks the entry as reversed and creates a new posted reversal entry with debit/credit swapped. Only posted entries can be reversed.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Reverse a journal entry",
"description": "Marks the entry as reversed and creates a new posted reversal entry with debit/credit swapped. Only posted entries can be reversed.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Reversal entry created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JournalEntry"
}
}
}
},
"404": {
"description": "Not found"
},
"422": {
"description": "Entry cannot be reversed in its current state"
}
}
}
GET /assets
List ERP fixed assets (the finance asset register)
Capitalised assets the company owns — IT Equipment, Furniture, Vehicle, Machinery, Building, Software License — with purchaseValue, currentValue, purchasedAt, location and status active/maintenance/retired/disposed. This is the finance-of-record register; live endpoint health for IT devices is in the dex app and consumable stock is in inventory. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ERP fixed assets (the finance asset register)",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Asset"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "Capitalised assets the company owns — IT Equipment, Furniture, Vehicle, Machinery, Building, Software License — with purchaseValue, currentValue, purchasedAt, location and status active/maintenance/retired/disposed. This is the finance-of-record register; live endpoint health for IT devices is in the dex app and consumable stock is in inventory. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key)."
}
POST /assets
Create ERP fixed asset record
Registers a newly acquired capital asset with its type, purchase value and location. Consumable or resold stock belongs in inventory rather than here.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create ERP fixed asset record",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Asset"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Asset"
}
}
}
}
},
"description": "Registers a newly acquired capital asset with its type, purchase value and location. Consumable or resold stock belongs in inventory rather than here."
}
GET /assets/{id}
Get asset
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get asset",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Asset"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /assets/{id}
Update asset
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update asset",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Asset"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Asset"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /assets/{id}
Delete asset
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete asset",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /purchase-requests
Create purchase request (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /createPurchaseRequest). Always returns a fixed PR-LEGACY id.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create purchase request (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /createPurchaseRequest). Always returns a fixed PR-LEGACY id.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"description": "Purchase request payload (ignored by stub)",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Submitted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"purchase_request": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "PR-LEGACY"
},
"status": {
"type": "string",
"example": "Submitted"
}
}
}
}
}
}
}
}
}
}
GET /purchase-requests/pending
List pending purchase requests (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /getPendingPurchases). Always returns an empty array.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List pending purchase requests (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /getPendingPurchases). Always returns an empty array.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"responses": {
"200": {
"description": "Empty list stub",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {}
}
}
}
}
}
}
GET /purchase-requests/history
List historical purchase requests (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /getHistoricalPurchases). Public (no auth required). Always returns an empty array.
Authentication
[]
Request, responses and operation details
{
"summary": "List historical purchase requests (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /getHistoricalPurchases). Public (no auth required). Always returns an empty array.",
"parameters": [],
"responses": {
"200": {
"description": "Empty list stub",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {}
}
}
}
}
},
"security": []
}
POST /approvals
Create approval (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /createApproval). Always returns APV-LEGACY id.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create approval (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /createApproval). Always returns APV-LEGACY id.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"description": "Approval payload (ignored by stub)",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "APV-LEGACY"
}
}
}
}
}
}
}
}
}
}
GET /approvals
List approvals (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /getApprovals). Always returns an empty data array.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "List approvals (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /getApprovals). Always returns an empty data array.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"responses": {
"200": {
"description": "Empty list stub",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "array",
"items": {}
}
}
}
}
}
}
}
}
PATCH /approvals/{approvalId}
Update approval (legacy)
Stub endpoint kept for backwards compatibility (legacyUrl: /updateApproval). Always returns an empty data object.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update approval (legacy)",
"description": "Stub endpoint kept for backwards compatibility (legacyUrl: /updateApproval). Always returns an empty data object.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "approvalId",
"required": true,
"schema": {
"type": "string"
},
"description": "Approval ID",
"example": "APV-0001"
}
],
"requestBody": {
"description": "Approval update payload (ignored by stub)",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object"
}
}
}
}
}
}
}
}