{"openapi":"3.0.0","info":{"title":"Purple Suite SCM 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/scm"}],"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":{"Supplier":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SUP-0001"},"name":{"type":"string","example":"Global Supply Co"},"status":{"type":"string","enum":["active","inactive","pending_approval","blacklisted"],"example":"active"},"category":{"type":"string","enum":["Raw Materials","Electronics","Office Supplies","Services","Packaging"],"example":"Electronics"},"country":{"type":"string","example":"USA"},"rating":{"type":"integer","minimum":1,"maximum":5,"example":4},"onboardedAt":{"type":"string","format":"date-time","example":"2025-05-01T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Contract":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CON-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"title":{"type":"string","example":"Master Supply Agreement - 2025"},"status":{"type":"string","enum":["draft","active","signed","expired","cancelled"],"example":"active"},"value":{"type":"number","example":250000},"startDate":{"type":"string","format":"date-time","example":"2025-01-01T00:00:00.000Z"},"endDate":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"autoRenew":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Shipment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SHIP-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"status":{"type":"string","enum":["pending","in_transit","out_for_delivery","delivered","exception","cancelled"],"example":"in_transit"},"carrier":{"type":"string","enum":["FedEx","UPS","DHL","USPS","Maersk","MSC"],"example":"FedEx"},"trackingNumber":{"type":"string","example":"1Z999AA10123456784"},"origin":{"type":"string","example":"Shanghai"},"destination":{"type":"string","example":"New York"},"estimatedDelivery":{"type":"string","format":"date-time","example":"2026-06-05T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"PurchaseOrder":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SCMPO-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"status":{"type":"string","enum":["draft","submitted","approved","received","cancelled"],"example":"draft"},"totalAmount":{"type":"number","example":45000},"lineItems":{"type":"integer","example":5},"deliveryDate":{"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}}},"QualityLot":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"QL-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"itemSku":{"type":"string","example":"SKU-A1B2C3D4"},"quantity":{"type":"integer","example":500},"status":{"type":"string","enum":["pending","in_inspection","passed","failed","quarantined"],"example":"pending"},"defectRate":{"type":"number","format":"float","example":1.25},"inspectedBy":{"type":"string","example":"qa1@company.com"},"receivedAt":{"type":"string","format":"date-time","example":"2026-05-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}}}},"paths":{"/suppliers":{"get":{"summary":"List suppliers","description":"Purple SCM suppliers are the vendors the company sources physical goods and services from, with category, country, quality rating, onboarding date and status (active | inactive | pending_approval | blacklisted). Supply-chain vendor master, distinct from ERP finance vendors and from CRM accounts (customers). Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","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/Supplier"}},"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)"}}},"post":{"summary":"Onboard supplier","description":"Add a supplier to the Purple SCM vendor master with its category, country and rating. New suppliers usually start pending_approval; search the existing list first to avoid duplicates.","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}}}},"/suppliers/{id}":{"get":{"summary":"Get supplier","description":"Fetch one supplier by id (e.g. SUP-0001) with its status, category, country and rating — the record behind sourcing, contract and quality questions.","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/Supplier"}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update supplier","description":"Partially update a supplier — change category, country or rating, or move its status. Setting status to 'blacklisted' stops future sourcing from them and should be deliberate.","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/Supplier"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete supplier","description":"Permanently delete a supplier from the vendor master. Destructive and irreversible, and it orphans their contracts, purchase orders, shipments and quality lots — set status to 'inactive' or 'blacklisted' instead.","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"}}}},"/contracts":{"get":{"summary":"List supplier contracts","description":"Purple SCM contracts are the commercial agreements with a supplier (master supply agreements and similar) with value, start/end dates, auto-renew flag and status draft | active | signed | expired | cancelled. Supplier agreements, not customer contracts or purchase orders. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","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/Contract"}},"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)"}}},"post":{"summary":"Create supplier contract","description":"Draft a new commercial agreement with a supplier, with its term dates, value and auto-renew flag. Also the correct move when an expired contract needs replacing, since expired contracts cannot be reactivated.","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contract"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contract"}}}}}}},"/contracts/{id}":{"get":{"summary":"Get supplier contract","description":"Fetch one contract by id (e.g. CON-0001) with its supplier, value, term dates, auto-renew flag and status — the check before raising a PO against it or letting it lapse.","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/Contract"}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update supplier contract","description":"Partially update a contract — revise value, dates or auto-renew, or move its status. The state machine blocks two moves (422): an expired contract cannot be returned to active/draft (create a new one), and a cancelled contract cannot be signed.","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/Contract"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contract"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete supplier contract","description":"Permanently delete a contract record. Destructive and irreversible, and it removes the commercial history — set status to 'cancelled' or let it expire instead.","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"}}}},"/shipments":{"get":{"summary":"List inbound shipments","description":"Purple SCM shipments are inbound freight from suppliers, with carrier (FedEx | UPS | DHL | USPS | Maersk | MSC), tracking number, origin, destination, estimated delivery and status pending | in_transit | out_for_delivery | delivered | exception | cancelled. Use for 'where is my delivery' questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","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/Shipment"}},"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)"}}},"post":{"summary":"Create shipment","description":"Record an inbound shipment from a supplier with its carrier, tracking number, origin, destination and estimated delivery.","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shipment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shipment"}}}}}}},"/shipments/{id}":{"get":{"summary":"Get shipment","description":"Fetch one shipment by id (e.g. SHIP-0001) with its carrier, tracking number, route, ETA and current status — the record to quote when tracking a delivery.","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/Shipment"}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update shipment","description":"Partially update a shipment — advance its status, correct the ETA or fix carrier/tracking details. Status only moves forward through pending → in_transit → out_for_delivery → delivered (going backwards is rejected with 422), and a delivered shipment cannot be cancelled.","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/Shipment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shipment"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete shipment","description":"Permanently delete a shipment record. Destructive and irreversible, and it loses the delivery trail — cancel an undelivered shipment via a status update instead.","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":{"get":{"summary":"List supply-chain purchase orders","description":"Purple SCM purchase orders are procurement POs raised on a supplier for goods (SCMPO-####), with total amount, line-item count, delivery date and status draft | submitted | approved | received | cancelled. This is the supply-chain view; the finance/ERP purchase-order ledger is a separate system with its own ids. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","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)"}}},"post":{"summary":"Create purchase order","description":"Raise a purchase order on a supplier with total amount, line items and requested delivery date. New POs start in draft/submitted; approve them by updating status.","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"}}}}}}},"/purchase_orders/{id}":{"get":{"summary":"Get purchase order","description":"Fetch one supply-chain purchase order by id (e.g. SCMPO-0001) with its supplier, total amount, line items, delivery date and status.","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 purchase order","description":"Partially update a purchase order, including approving it (status 'approved') or cancelling it. The state machine rejects (422) cancelling a PO already received or in transit, and approving a cancelled PO.","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","description":"Permanently delete a purchase order. Destructive and irreversible, and it breaks the audit trail against the supplier — set status to 'cancelled' instead.","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"}}}},"/quality_lots":{"get":{"summary":"List quality inspection lots","description":"Purple SCM quality lots are received batches of goods under incoming inspection, with item SKU, quantity, supplier, defect rate, inspector and status pending | in_inspection | passed | failed | quarantined. Use these for supplier quality and defect-rate questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","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/QualityLot"}},"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)"}}},"post":{"summary":"Create quality lot","description":"Register a received batch for incoming quality inspection, with its SKU, quantity and supplier. It starts pending; record the outcome by updating its status and defect rate.","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QualityLot"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QualityLot"}}}}}}},"/quality_lots/{id}":{"get":{"summary":"Get quality lot","description":"Fetch one quality lot by id (e.g. QL-0001) with its SKU, quantity, supplier, defect rate, inspector and inspection status.","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/QualityLot"}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update quality lot","description":"Record an inspection outcome on a lot — set status to in_inspection, passed, failed or quarantined, and write the defect rate and inspector. Quarantining or failing a lot holds the goods, so state the reason.","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/QualityLot"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QualityLot"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete quality lot","description":"Permanently delete a quality lot record. Destructive and irreversible, and it erases the supplier's quality evidence — record a failed or quarantined outcome instead.","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"}}}},"/openapi.json":{"get":{"summary":"Get OpenAPI spec","description":"Returns the OpenAPI 3.0 specification for this API. No authentication required.","security":[],"responses":{"200":{"description":"OpenAPI spec","content":{"application/json":{"schema":{"type":"object"}}}}}}}}}