{"openapi":"3.0.0","info":{"title":"Purple Suite ERP API","version":"1.0.0","description":"\n\n---\n\n**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`."},"servers":[{"url":"https://marketplace.moveworks.com/api/purple-suite/erp"}],"security":[{"patAuth":[]}],"components":{"securitySchemes":{"patAuth":{"type":"http","scheme":"bearer"}},"parameters":{"InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"ImpersonateUser":{"in":"header","name":"X-PS-Impersonate-User","required":false,"schema":{"type":"string","format":"email"},"description":"Optional. Act as a specific user in the target instance — the value is that user's email (must exist in the instance). Resolution order for the effective user: (1) this header, if set; (2) the instance's configured default MCP user; (3) otherwise no user is bound. User-scoped paths (e.g. /users/{email}/…) also accept the literal \"me\", and Graph-style apps serve /me/… twins of those paths (Graph: /me ≡ /users/{signed-in-user}) — both resolve to whatever this header (or the default MCP user) points at. An explicit path email always wins over the header. The unified MCP server does not send this header — it relies on the per-instance default — so it is primarily for direct REST/HTTP callers."}},"schemas":{"PurchaseOrder":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"vendor":{"type":"string","example":"Acme Corp"},"status":{"type":"string","enum":["draft","pending_approval","approved","sent","partially_received","received","cancelled"],"example":"draft"},"totalAmount":{"type":"number","example":12500},"currency":{"type":"string","example":"USD"},"lineItems":{"type":"integer","example":3},"approver":{"type":"string","example":"finance@acme.com"},"expectedDelivery":{"type":"string","format":"date-time","example":"2026-06-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Invoice":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"vendor":{"type":"string","example":"Acme Corp"},"status":{"type":"string","enum":["draft","pending_payment","paid","void","overdue"],"example":"draft"},"amount":{"type":"number","example":4200},"currency":{"type":"string","example":"USD"},"dueDate":{"type":"string","format":"date-time","example":"2026-06-30T00:00:00.000Z"},"poId":{"type":"string","description":"Purchase order this invoice is linked to (used for 3-way match)","example":"PO-0001"},"notes":{"type":"string","description":"Free-text notes; the only field writable on a voided invoice","example":"Approved by finance"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"InventoryItem":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"itemName":{"type":"string","example":"Laptop Pro 2024"},"sku":{"type":"string","example":"SKU-1A2B3C4D"},"quantity":{"type":"integer","example":50},"reorderPoint":{"type":"integer","example":10},"unitCost":{"type":"number","example":1200},"warehouseLocation":{"type":"string","example":"WH-EAST"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"JournalEntry":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"type":{"type":"string","enum":["debit","credit","adjustment","accrual","reversal"],"example":"accrual"},"debitAccount":{"type":"string","example":"1000-Cash"},"creditAccount":{"type":"string","example":"2000-AP"},"amount":{"type":"number","example":5000},"description":{"type":"string","example":"Monthly accrual"},"status":{"type":"string","enum":["draft","posted","reversed"],"example":"draft"},"period":{"type":"string","example":"2026-06"},"postedAt":{"type":"string","format":"date-time","description":"ISO timestamp when the entry was posted; set automatically on posted and reversal entries","readOnly":true},"reversalOf":{"type":"string","description":"ID of the original entry this reversal was created from","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Asset":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"MacBook Pro"},"type":{"type":"string","enum":["IT Equipment","Furniture","Vehicle","Machinery","Building","Software License"],"example":"IT Equipment"},"status":{"type":"string","enum":["active","maintenance","retired","disposed"],"example":"active"},"purchaseValue":{"type":"number","example":2500},"currentValue":{"type":"number","example":1800},"purchasedAt":{"type":"string","format":"date-time","description":"Date the asset was originally purchased","example":"2025-01-15T00:00:00.000Z"},"location":{"type":"string","example":"HQ"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}}}},"paths":{"/purchase-orders":{"get":{"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":{"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."}},"/purchase-orders/{id}":{"get":{"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":{"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":{"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"}}}},"/purchase-orders/{id}/verify-match":{"post":{"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"}}}},"/invoices":{"get":{"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":{"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."}},"/invoices/{id}":{"get":{"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":{"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":{"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"}}}},"/inventory":{"get":{"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":{"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."}},"/inventory/{id}":{"get":{"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":{"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":{"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"}}}},"/journal-entries":{"get":{"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":{"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."}},"/journal-entries/{id}":{"get":{"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":{"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":{"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"}}}},"/journal-entries/{id}/reverse":{"post":{"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"}}}},"/assets":{"get":{"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":{"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."}},"/assets/{id}":{"get":{"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":{"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":{"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"}}}},"/purchase-requests":{"post":{"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"}}}}}}}}}}},"/purchase-requests/pending":{"get":{"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":{}}}}}}}},"/purchase-requests/history":{"get":{"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":[]}},"/approvals":{"post":{"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":{"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":{}}}}}}}}}},"/approvals/{approvalId}":{"patch":{"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"}}}}}}}}}}}