{"openapi":"3.0.0","info":{"title":"Purple Suite CRM 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/crm"}],"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":{"Account":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Global Solutions"},"industry":{"type":"string","enum":["Technology","Healthcare","Finance","Retail","Manufacturing","Education","Government","Media"],"example":"Technology"},"territory":{"type":"string","enum":["North America","EMEA","APAC","LATAM"],"example":"North America"},"status":{"type":"string","enum":["customer","prospect","partner","inactive"],"example":"customer"},"owner":{"type":"string","example":"alice@example.com"},"employees":{"type":"integer","example":500},"annualRecurringRevenue":{"type":"number","example":1200000},"monthlyRecurringRevenue":{"type":"number","example":100000},"averageDealSize":{"type":"number","example":50000},"totalOpportunities":{"type":"integer","example":12},"openOpportunities":{"type":"integer","example":4},"closedWonOpportunities":{"type":"integer","example":6},"closedLostOpportunities":{"type":"integer","example":2},"churnRate":{"type":"number","example":3.5},"netPromoterScore":{"type":"integer","example":45},"supportTickets":{"type":"integer","example":7},"officeCount":{"type":"integer","example":3},"marketSharePct":{"type":"number","example":12.5},"growthRate":{"type":"number","example":18},"fundingAmount":{"type":"number","example":10000000},"lastContactDate":{"type":"string","format":"date-time","example":"2026-05-01T00:00:00.000Z"},"nextRenewalDate":{"type":"string","format":"date-time","example":"2026-11-25T00:00:00.000Z"},"lastModifiedAt":{"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}}},"Contact":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"firstName":{"type":"string","example":"James"},"lastName":{"type":"string","example":"Chen"},"email":{"type":"string","format":"email","example":"james.chen@example.com"},"title":{"type":"string","enum":["CEO","CTO","VP Sales","Director","Manager","Senior Manager","Engineer","Analyst"],"example":"VP Sales"},"accountId":{"type":"string","example":"ACC-0001"},"phone":{"type":"string","example":"+1-415-555-1234"},"status":{"type":"string","enum":["active","inactive"],"example":"active"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Opportunity":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Enterprise New Business - Q2 2024"},"accountId":{"type":"string","example":"ACC-0001"},"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"qualification"},"amount":{"type":"number","example":75000},"probability":{"type":"integer","example":50},"closeDate":{"type":"string","format":"date-time","example":"2026-06-28T00:00:00.000Z"},"owner":{"type":"string","example":"alice@example.com"},"stageHistory":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"notes":{"type":"string","nullable":true},"changedAt":{"type":"string","format":"date-time"}}},"description":"Populated automatically by /advance-stage"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Lead":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"firstName":{"type":"string","example":"Sarah"},"lastName":{"type":"string","example":"Rodriguez"},"company":{"type":"string","example":"TechCo"},"email":{"type":"string","format":"email","example":"lead0@prospect.com"},"phone":{"type":"string","example":"+1-415-555-9876","description":"Optional phone number. If present, copied to the Contact created by /convert."},"title":{"type":"string","example":"VP Engineering","description":"Optional job title. If present, copied to the Contact created by /convert."},"accountId":{"type":"string","example":"ACC-0001","description":"Optional linked account ID. If present, copied to the Contact created by /convert."},"status":{"type":"string","enum":["new","contacted","qualified","converted","disqualified","dead"],"example":"new"},"source":{"type":"string","enum":["Web Form","Trade Show","Referral","Cold Call","LinkedIn","Email Campaign"],"example":"Web Form"},"score":{"type":"integer","example":72},"convertedTo":{"type":"object","readOnly":true,"nullable":true,"properties":{"contactId":{"type":"string"},"opportunityId":{"type":"string","nullable":true}},"description":"Set automatically by /convert"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Case":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"subject":{"type":"string","example":"Technical support"},"accountId":{"type":"string","example":"ACC-0001"},"status":{"type":"string","enum":["open","in_progress","waiting_on_customer","resolved","closed"],"example":"open"},"priority":{"type":"string","enum":["low","medium","high","critical"],"example":"medium"},"assignee":{"type":"string","example":"alice@example.com"},"resolvedAt":{"type":"string","format":"date-time","nullable":true,"example":null},"reopenReason":{"type":"string","example":"Customer reported issue recurred","description":"Required when reopening a closed case (stored field name). See also: reopen_reason."},"reopen_reason":{"type":"string","example":"Customer reported issue recurred","description":"Input alias for reopenReason accepted by PATCH /cases/:id. Normalised to reopenReason before storage. Use reopenReason in responses."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Activity":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"type":{"type":"string","enum":["call","email","meeting","task","note"],"example":"call"},"subject":{"type":"string","example":"Follow-up call"},"accountId":{"type":"string","example":"ACC-0001"},"owner":{"type":"string","example":"alice@example.com"},"completed":{"type":"boolean","example":false},"dueDate":{"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}}}}},"paths":{"/accounts":{"get":{"summary":"List CRM accounts (customer companies)","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/Account"}},"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":"Sales-relationship records for companies this org sells to — customers, prospects and partners — with owner, territory, industry, ARR/MRR and renewal dates. These are not bank deposit accounts (see the retail-banking app) and not user/login accounts (see the identity 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 CRM account (customer company)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}}},"description":"Adds a new company record to the CRM. Use this for an organisation you sell to; an individual person at a known company belongs in contacts, and an unqualified inbound person belongs in leads."}},"/accounts/{id}":{"get":{"summary":"Get CRM account by id","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/Account"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer company (e.g. ACC-0001) with its firmographics and revenue rollups. Use this when you already have the account id; to find an account by name or attribute, use the list tool with _search or _filter."},"patch":{"summary":"Update CRM account","description":"Partial update of a customer company — e.g. reassign owner, change status (customer/prospect/partner/inactive), territory, or revenue fields. Send only the fields you want to change.","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/Account"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete CRM account (destructive)","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"}},"description":"Permanently removes the company record; this cannot be undone and related contacts, opportunities and cases are not cascaded or cleaned up. Prefer updating status to 'inactive' unless the record genuinely must be purged."}},"/contacts":{"get":{"summary":"List CRM contacts (named people at customer accounts)","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/Contact"}},"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":"External customer-side individuals — buyers, champions, admins — each linked to an account via accountId, with title, email, phone and active/inactive status. Not employees (see the hris and identity apps) and not unqualified prospects (see leads). 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 CRM contact (person at a customer account)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}}},"description":"Adds a person who already belongs to a known account. For an unqualified inbound prospect create a lead instead, and to promote an existing lead use the lead convert tool rather than creating a contact by hand."}},"/contacts/{id}":{"get":{"summary":"Get CRM contact by id","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/Contact"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer-side person record by id, including their accountId, title and contact details."},"patch":{"summary":"Update CRM contact","description":"Partial update of a customer-side person — e.g. new title, email, phone, owning account, or flipping status to inactive.","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/Contact"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete CRM contact (destructive)","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"}},"description":"Permanently removes the person record; this cannot be undone. Prefer setting status to 'inactive' so engagement history stays attributable."}},"/opportunities":{"get":{"summary":"List CRM opportunities (sales deals in the pipeline)","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/Opportunity"}},"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":"Revenue deals tied to an account, with stage, amount, probability, closeDate, owner and a server-maintained stageHistory. This is the sales pipeline — customer support tickets are cases and logged touchpoints are activities. 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). Example: _filter=\"stage ne 'closed_lost' and amount gt 50000\"."},"post":{"summary":"Create CRM opportunity (new sales deal)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}}},"description":"Opens a new pipeline deal against an account with a stage, amount and close date. To move an existing deal forward use the advance-stage tool rather than creating a second opportunity."}},"/opportunities/{id}":{"get":{"summary":"Get CRM opportunity by id","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/Opportunity"}}}},"404":{"description":"Not found"}},"description":"Fetches one pipeline deal including its stageHistory (the audit trail of stage moves recorded by the advance-stage tool)."},"patch":{"summary":"Update CRM opportunity (non-stage fields)","description":"Partial update of deal attributes — amount, probability, closeDate, owner, name. To change the pipeline stage prefer the advance-stage tool, which enforces the stage order and appends to stageHistory; this tool does neither.","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/Opportunity"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete CRM opportunity (destructive)","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"}},"description":"Permanently removes the deal and its stage history; this cannot be undone. To record a deal that did not land, advance its stage to closed_lost instead."}},"/opportunities/{id}/advance-stage":{"post":{"summary":"Advance a CRM opportunity to a new pipeline stage","description":"The preferred way to move a deal: sets the stage, appends a timestamped stageHistory entry with optional notes, and enforces pipeline order. Forward-only and terminal — moving backwards is rejected, and a closed_won/closed_lost deal cannot be advanced at all. Use PATCH /opportunities/{id} for non-stage edits.","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"proposal"},"notes":{"type":"string","example":"Customer responded positively to initial pitch"}},"required":["stage"]}}}},"responses":{"200":{"description":"Updated opportunity with new stage and stageHistory entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}},"400":{"description":"Invalid stage value"},"404":{"description":"Not found"},"422":{"description":"Stage transition not allowed (opportunity is closed or move is backwards)"}}}},"/leads":{"get":{"summary":"List CRM leads (unqualified inbound prospects)","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/Lead"}},"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":"Top-of-funnel people not yet tied to an account, with source (web form, trade show, referral…), score and status new → contacted → qualified → converted/disqualified/dead. Once qualified, a lead becomes a contact (and optionally an opportunity) via the convert tool. 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 CRM lead (raw inbound prospect)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Lead"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Lead"}}}}},"description":"Captures a new unqualified prospect with their company, source and score. Use contacts instead when the person already belongs to a known customer account."}},"/leads/{id}":{"get":{"summary":"Get CRM lead by id","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/Lead"}}}},"404":{"description":"Not found"}},"description":"Fetches one prospect record, including convertedTo once the lead has been run through the convert tool."},"patch":{"summary":"Update CRM lead","description":"Partial update — e.g. move status from new to contacted or qualified, adjust score, or fill in phone/title/accountId. Do not set status to 'converted' here; use the convert tool so the contact (and optional opportunity) are actually created.","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/Lead"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Lead"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete CRM lead (destructive)","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"}},"description":"Permanently removes the prospect record; this cannot be undone. Prefer status 'disqualified' or 'dead' so funnel conversion rates stay accurate."}},"/leads/{id}/convert":{"post":{"summary":"Convert a CRM lead into a contact (and optionally an opportunity)","description":"The one-way qualification step: creates a Contact from the lead's fields, additionally creates an Opportunity when opportunity_data is supplied, and stamps the lead as 'converted'. Not repeatable and not reversible — a lead that is already converted or disqualified is rejected.","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"opportunity_data":{"type":"object","description":"Optional. If provided, an Opportunity is created by spreading these fields.","properties":{"name":{"type":"string","example":"Enterprise New Business - Q3 2026"},"accountId":{"type":"string","example":"ACC-0001"},"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"prospecting"},"amount":{"type":"number","example":50000},"probability":{"type":"integer","example":10},"closeDate":{"type":"string","format":"date-time","example":"2026-09-30T00:00:00.000Z"},"owner":{"type":"string","example":"alice@example.com"}}}}}}}},"responses":{"200":{"description":"Converted lead with the created contact and optional opportunity","content":{"application/json":{"schema":{"type":"object","properties":{"contact":{"$ref":"#/components/schemas/Contact"},"opportunity":{"$ref":"#/components/schemas/Opportunity","nullable":true},"lead":{"$ref":"#/components/schemas/Lead"}}}}}},"404":{"description":"Not found"},"422":{"description":"Lead cannot be converted (already converted or disqualified)"}}}},"/cases":{"get":{"summary":"List CRM cases (customer support tickets)","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/Case"}},"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":"Issues reported by a customer against an account, with subject, priority, assignee and status open → in_progress → resolved → closed. These are customer-facing support cases; internal employee IT tickets live in the itsm 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 CRM case (customer support ticket)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Case"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Case"}}}}},"description":"Logs a new customer-reported issue against an account with a subject and priority. For an internal employee IT problem create an itsm incident instead."}},"/cases/{id}":{"get":{"summary":"Get CRM case by id","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/Case"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer support case with its status, priority, assignee and resolution timestamps."},"patch":{"summary":"Update or transition a CRM case","description":"Partial update, and the only way to move a case through its lifecycle. State-machine rules are enforced: an 'open' case cannot jump straight to 'closed' (route it via in_progress or resolved), and reopening a 'closed' case requires reopenReason (alias reopen_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/Case"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Case"}}}},"404":{"description":"Not found"},"422":{"description":"State-machine violation — e.g. closing an open case directly, or reopening without reopenReason"}}},"delete":{"summary":"Delete CRM case (destructive)","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"}},"description":"Permanently removes the support case and its history; this cannot be undone. Resolving or closing the case is almost always the right action instead."}},"/activities":{"get":{"summary":"List CRM activities (logged calls, emails, meetings, tasks, notes)","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/Activity"}},"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":"The touchpoint timeline attached to an account: type, subject, owner, dueDate and a completed flag. Use it for engagement history or to find open follow-ups; real calendar invites live in the calendar 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":"Log a CRM activity (call, email, meeting, task or note)","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Activity"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Activity"}}}}},"description":"Records a touchpoint that happened, or schedules a follow-up task, against an account. This only writes a CRM record — it does not send an email or book a meeting."}},"/activities/{id}":{"get":{"summary":"Get CRM activity by id","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/Activity"}}}},"404":{"description":"Not found"}},"description":"Fetches one logged touchpoint or follow-up task from an account's timeline."},"patch":{"summary":"Update CRM activity","description":"Partial update of a logged touchpoint — most often flipping completed to true when a follow-up is done, or changing dueDate, owner or subject.","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/Activity"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Activity"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete CRM activity (destructive)","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"}},"description":"Permanently removes the touchpoint from the account timeline; this cannot be undone. Mark a task completed instead of deleting it when the work simply finished."}},"/recent-campaign":{"get":{"summary":"Get most recent campaign (legacy stub)","description":"Legacy compatibility endpoint. Returns a static most-recent campaign record. No authentication required.","parameters":[],"responses":{"200":{"description":"Most recent campaign","content":{"application/json":{"schema":{"type":"object","properties":{"campaign":{"type":"object","properties":{"id":{"type":"string","example":"campaign-2025-q1"},"name":{"type":"string","example":"Q1 2025 Referral Campaign"},"status":{"type":"string","example":"active"}}}}}}}}},"security":[]}},"/engagement":{"get":{"summary":"Get customer engagement data (legacy stub)","description":"Legacy compatibility endpoint. Returns static engagement metrics. No authentication required.","parameters":[],"responses":{"200":{"description":"Engagement metrics","content":{"application/json":{"schema":{"type":"object","properties":{"engagement":{"type":"object","properties":{"totalUsers":{"type":"integer","example":1250},"activeUsers":{"type":"integer","example":890},"engagementRate":{"type":"number","example":71.2}}}}}}}}},"security":[]}}}}