{"openapi":"3.1.0","info":{"title":"HQ API","description":"Public HTTP API for HQ. Authenticate with a Personal Access Token (`Authorization: Bearer hq_pat_...`) for server-side integrations, or an OAuth 2.1 authorization-code + PKCE flow for browser apps acting on a user's behalf. Both grant from the same resource:action scope vocabulary; an endpoint's required scope is listed under its `security`.","license":{"name":"Apache-2.0","identifier":"Apache-2.0"},"version":"1.0.0"},"servers":[{"url":"https://api.hq.zone","description":"HQ API (production)"}],"paths":{"/v1/admin/audit":{"get":{"tags":["admin"],"summary":"List audit log","description":"Returns a paginated, recency-first list of audit-log events for the caller's own\nworkspace, newest first. Admin only. Supports optional filtering by event kind,\noutcome, actor kind, agent id, and conversation id, a free-text query matched\nagainst the event target and intent, a lookback window in days (default 90; 0 or\nnegative means all time), and page/page_size paging (page_size defaults to 50 and is\nclamped to 1-200). Each event includes its sequence number, hash-chain fields, and\nredacted details, alongside the unpaginated total.","operationId":"list_audit","parameters":[{"name":"page","in":"query","description":"1-based page. Defaults to 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Defaults to 50, clamped to [1, 200].","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"days","in":"query","description":"Lookback window in days for partition pruning. Defaults to 90.\n`0` (or negative) means \"all time\" (binds a 1970 floor).","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"kind","in":"query","description":"Exact-match filters (all optional).","required":false,"schema":{"type":["string","null"]}},{"name":"outcome","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"actor_kind","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"agent_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"conversation_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"q","in":"query","description":"Free-text needle matched (ILIKE) against `target` and `intent`.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Paginated audit events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/admin/audit/verify":{"get":{"tags":["admin"],"summary":"Verify audit chain","description":"Re-walks the workspace's tamper-evident audit hash chain and reports whether it is\nintact. Admin only. Checks the most recent entries (the limit query parameter\ndefaults to 10000 and is clamped to 1-100000), recomputing each entry's hash and\nverifying it links to its predecessor. Returns whether the chain is ok, how many\nentries were checked, the first and last sequence numbers covered, and, if broken,\nthe sequence number where it first fails and whether the break is a content or\nlinkage mismatch.","operationId":"verify_audit","parameters":[{"name":"limit","in":"query","description":"Verify the most recent `limit` entries by `seq` (default 10000, max\n100000). The chain is walked in ascending `seq` order; the entry at\n`min_seq - 1` is fetched as the linkage anchor.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Chain verification result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/admin/users/{id}/erase":{"post":{"tags":["admin"],"summary":"Erase another user (admin)","description":"`POST /v1/admin/users/{id}/erase` - admin-erasure on behalf of\nanother user. Requires `is_admin = TRUE` on the caller's row.\nRefuses self-erase via this route (use /v1/api/me/erase) so the\naudit trail consistently records `actor == subject` only for the\nexplicit self path.","operationId":"erase_user","parameters":[{"name":"id","in":"path","description":"Subject user id to erase","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EraseReq"}}},"required":true},"responses":{"200":{"description":"Erasure summary","content":{"application/json":{"schema":{}}}},"400":{"description":"Cannot self-erase via the admin route"},"403":{"description":"Admin role required"},"404":{"description":"User not in this tenant"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agent-browser/ask":{"post":{"tags":["integrations"],"summary":"Ask using the user's browser","description":"The dedicated browser path. The extension's \"use my browser\" button POSTs\nhere, PAT-authed (the same token the WebSocket uses). This is the ONLY entry\nthat runs an agent scoped to the local-browser tools (computer_*) + the\nbrowser prompt - normal chat (web / Slack) never sees them, because\nhq:computer is `default_enabled = FALSE` and the gateway only widens the\nscope when this turn's `browser_session` flag is set. Runs synchronously and\nreturns the agent's reply.","operationId":"browser_ask","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskReq"}}},"required":true},"responses":{"200":{"description":"The agent's reply, run against the caller's connected local browser (computer_* tools + browser prompt)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResp"}}}},"403":{"description":"Missing/invalid PAT, or no live browser connection for this user"}},"security":[{"bearer_pat":[]}]}},"/v1/agent-browser/connect":{"get":{"tags":["integrations"],"summary":"Connect the browser extension","description":"Opens the WebSocket the HQ browser extension dials out to in order to receive remote\nbrowser-control commands and stream back results. The extension authenticates by\npassing its personal access token in the Sec-WebSocket-Protocol header as\nhq-pat.<token>, and the request is answered with a 101 protocol-upgrade rather than\na normal JSON response. There is one live connection per user; the per-frame message\nprotocol is not described by the API schema.","operationId":"browser_connect","responses":{"101":{"description":"WebSocket upgrade. The HQ extension dials out here (PAT in Sec-WebSocket-Protocol as `hq-pat.<token>`) to receive `computer.*` commands. Frames are the WebDriver-BiDi-shaped local_browser_protocol (see docs/local-browser-control.md); OpenAPI cannot describe the per-frame protocol."}},"security":[{"bearer_pat":[]}]}},"/v1/agents":{"get":{"tags":["agents"],"summary":"List agents","description":"Returns every agent configured in the caller's workspace, including each agent's\nslug, display name, description, system-prompt instructions, personality settings\n(tone, verbosity, cautions, languages), aliases, runtime profile, selected model,\ndefault flag, status, and avatar URL. Results are scoped to the caller's own\nworkspace and ordered with the default agent first, then enabled agents, then by\nslug. Requires the agents:read scope.","operationId":"list_agents","responses":{"200":{"description":"This workspace's agents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]},"post":{"tags":["agents"],"summary":"Create an agent","description":"Creates a new agent in the caller's workspace from the supplied slug, display name,\nand optional description, instructions, personality settings, aliases, runtime\nprofile, model, avatar URL, and initial enabled skill/MCP subsets, returning the\ncreated agent. The slug and aliases must be lowercase 2-32 character handles\n(letters, digits, hyphens), must not use reserved words, and must not collide with\nanother agent in the workspace; if no avatar URL is given one is generated\nautomatically. Admin only.","operationId":"create_agent","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReq"}}},"required":true},"responses":{"200":{"description":"The created agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"400":{"description":"Invalid slug, alias conflict, or bad field"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}":{"delete":{"tags":["agents"],"summary":"Disable an agent","description":"Disables the named agent by setting its status to disabled so it no longer routes on\nany surface, and returns the now-disabled agent. The default agent cannot be\ndisabled; promote another agent to default first (returns 400). Returns 404 if the\nagent is not in the caller's workspace. Admin only.","operationId":"disable_agent","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The now-disabled agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"400":{"description":"Cannot disable the default agent"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"patch":{"tags":["agents"],"summary":"Update an agent","description":"Updates the named agent's editable fields (slug, display name, description,\ninstructions, tone, verbosity, cautions, languages, aliases, runtime profile, model,\navatar URL) and returns the updated agent; only the fields you send are changed, and\nan explicit null clears nullable fields back to their default. Renaming the slug\nautomatically preserves the old slug as an alias so existing references still route.\nValidation matches creation (slug/alias rules and collision checks); returns 404 if\nthe agent is not in the caller's workspace. Admin only.","operationId":"update_agent","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReq"}}},"required":true},"responses":{"200":{"description":"The updated agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"400":{"description":"Invalid slug / alias conflict / bad field"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/avatar":{"post":{"tags":["agents"],"summary":"Upload an agent avatar","description":"`POST /v1/agents/{id}/avatar` - admin uploads an avatar image.\nMirrors the business_profile_api logo upload: base64 in, decoded\n+ validated locally, then handed to the internal artifacts\npublisher which writes Trove + the row. Resulting download URL is\nstored as the agent's `avatar_url`.","operationId":"upload_avatar","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarUploadReq"}}},"required":true},"responses":{"200":{"description":"The agent, with its new avatar_url","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"400":{"description":"Empty / oversized / unsupported image"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/default":{"post":{"tags":["agents"],"summary":"Set the default agent","description":"Promotes the named agent to be the workspace's default agent, atomically clearing\nthe default flag from whichever agent previously held it, and returns the\nnow-default agent. The target must be enabled; a disabled agent cannot be made\ndefault (returns 400). Returns 404 if the agent is not in the caller's workspace.\nAdmin only.","operationId":"set_default","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The now-default agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"400":{"description":"Cannot make a disabled agent the default"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/enable":{"post":{"tags":["agents"],"summary":"Enable an agent","description":"Re-enables a previously disabled agent by setting its status back to enabled,\nrestoring routing on all surfaces, and returns the re-enabled agent. Idempotent:\ncalling it on an already-enabled agent is a no-op. Returns 404 if the agent is not\nin the caller's workspace. Admin only.","operationId":"enable_agent","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The re-enabled agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRow"}}}},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/mcp":{"get":{"tags":["agents"],"summary":"List an agent's MCP servers","description":"Returns the agent's enabled MCP-server subset as a list of server slugs. An empty\nlist means the agent uses all MCP servers installed for the workspace. Returns 404\nif the agent is not in the caller's workspace. Requires the agents:read scope.","operationId":"list_mcp","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Enabled MCP subset (empty = all installed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpResp"}}}},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]},"put":{"tags":["agents"],"summary":"Replace an agent's MCP servers","description":"Replaces the agent's enabled MCP-server subset with the supplied list of server\nslugs and returns the new subset; sending an empty list resets the agent to use all\ninstalled MCP servers. Every requested slug must be an MCP server currently\ninstalled for the workspace, otherwise the request is rejected (returns 400).\nReturns 404 if the agent is not in the caller's workspace. Admin only.","operationId":"replace_mcp","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceMcpReq"}}},"required":true},"responses":{"200":{"description":"The new enabled MCP subset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpResp"}}}},"400":{"description":"A requested MCP server is not installed for this tenant"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/skills":{"get":{"tags":["agents"],"summary":"List an agent's skills","description":"Returns the agent's enabled skill subset as a list of skill slugs. An empty list\nmeans the agent uses all skills installed for the workspace. Returns 404 if the\nagent is not in the caller's workspace. Requires the agents:read scope.","operationId":"list_skills","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Enabled skill subset (empty = all installed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillsResp"}}}},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]},"put":{"tags":["agents"],"summary":"Replace an agent's skills","description":"Replaces the agent's enabled skill subset with the supplied list of skill slugs and\nreturns the new subset; sending an empty list resets the agent to use all installed\nskills. Every requested slug must be a skill currently installed for the workspace,\notherwise the request is rejected (returns 400). Returns 404 if the agent is not in\nthe caller's workspace. Admin only.","operationId":"replace_skills","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceSkillsReq"}}},"required":true},"responses":{"200":{"description":"The new enabled skill subset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillsResp"}}}},"400":{"description":"A requested skill is not installed for this tenant"},"404":{"description":"No such agent in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/agents/{id}/suggest-instructions":{"post":{"tags":["agents"],"summary":"Suggest agent instructions","description":"`POST /v1/agents/{id}/suggest-instructions`\n\nReads the tenant's business profile + corpus signals + the agent's\ncurrent role, asks Claude to draft a tailored system prompt with\nprivacy guards in place. Returns the draft for the admin to\nreview-and-save. Refuses with 503 when no Anthropic key is\nconfigured at boot (dev environment without ANTHROPIC_API_KEY).","operationId":"suggest_instructions","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A draft tailored system prompt for review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestResp"}}}},"404":{"description":"No such agent in this workspace"},"500":{"description":"agent-tailor disabled (no Anthropic key)"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/apps":{"get":{"tags":["conversations"],"summary":"List apps","description":"Returns the workspace's catalog of persistent app surfaces, visible to any member of\nthe workspace, excluding destroyed apps and ordered by most recent activity. Each\napp includes its id, slug, state, visibility (public or private), run mode, declared\nport, originating conversation id (null if that conversation was deleted), a\nready-to-open visitor URL, and creation/last-activity timestamps. Also returns the\nworkspace's app quota and how many apps currently count against it.","operationId":"list_apps","responses":{"200":{"description":"Workspace app catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppsResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/artifacts":{"get":{"tags":["conversations"],"summary":"List artifacts","description":"Lists delivered artifacts (agent-produced deliverables) visible to the caller across\ntheir workspace, newest first and capped at 500. A caller sees artifacts from\nconversations they participate in, plus detached artifacts (whose conversation was\ndeleted) that they originally produced; system assets such as avatars are excluded.\nEach entry includes name, content type, size, optional caption, a sensitive flag,\ncreation time, originating conversation id and title (null once detached), and a\ndownload URL.","operationId":"list_artifacts","responses":{"200":{"description":"Delivered artifacts visible to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactsResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/artifacts/{id}":{"delete":{"tags":["conversations"],"summary":"Delete an artifact","description":"Permanently deletes a delivered artifact and its stored bytes. Authorized like the\nartifact listing: the caller must participate in the artifact's conversation, or,\nonce detached, be the artifact's original producer (otherwise 403). Deleting an\nattached artifact also removes it from the conversation transcript. Returns the\nartifact id and a deleted flag; returns 404 if the artifact is not found in the\ncaller's workspace.","operationId":"delete_artifact","parameters":[{"name":"id","in":"path","description":"Artifact id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Artifact deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteArtifactResp"}}}},"403":{"description":"Not allowed"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/artifacts/{id}/download":{"get":{"tags":["conversations"],"summary":"Download an artifact","description":"Downloads the bytes of the named artifact, streamed back with the artifact's\noriginal Content-Type and a Content-Disposition: attachment header carrying its\nfilename. Scoped to the caller's own workspace: an artifact belonging to another\nworkspace returns 404.","operationId":"download_artifact","parameters":[{"name":"id","in":"path","description":"Artifact id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The artifact bytes, streamed, with the original Content-Type and a Content-Disposition: attachment carrying the filename","content":{"application/octet-stream":{}}},"404":{"description":"No such artifact in the caller's tenant"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/ask":{"post":{"tags":["conversations"],"summary":"Start a fast ask","description":"Create an empty `mode='fast'` conversation and return its id. The\nStudio \"New ask\" button calls this, then navigates to the thread; the\nfirst message runs on the fast lane because the conversation is fast.","operationId":"new_ask","responses":{"200":{"description":"New fast conversation id","content":{"application/json":{"schema":{}}}},"402":{"description":"Out of credits"},"503":{"description":"Quick chat not configured"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/billing/checkout":{"post":{"tags":["billing"],"summary":"Start a checkout session","description":"Starts a subscription upgrade by creating a checkout session for the named plan and\nreturns a hosted checkout URL to redirect the customer to. Workspace admin only. The\nplan_slug must reference an active plan that has a configured price (otherwise 400);\nthe actual plan provisioning happens asynchronously once payment completes. Returns\n503 if billing is not configured.","operationId":"checkout","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutReq"}}},"required":true},"responses":{"200":{"description":"Stripe Checkout URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlResp"}}}},"400":{"description":"Plan has no Stripe price"},"403":{"description":"Admin role required"},"503":{"description":"Billing not configured"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/billing/portal":{"post":{"tags":["billing"],"summary":"Open the billing portal","description":"Creates a hosted billing-portal session for the caller's workspace and returns its\nURL, where an admin can manage payment methods, view invoices, and change or cancel\nthe subscription. Workspace admin only. Works even for workspaces that have never\npurchased (a billing customer is created on demand). Returns 503 if billing is not\nconfigured.","operationId":"portal","responses":{"200":{"description":"Stripe Billing Portal URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlResp"}}}},"403":{"description":"Admin role required"},"503":{"description":"Billing not configured"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/billing/topup":{"post":{"tags":["billing"],"summary":"Buy a credit pack","description":"Buy a one-time credit pack (billing D). Available to ANY tier - a free-tier\naccount gets a Stripe customer minted on demand, same as checkout. The\n`+topup` lands in credit_ledger when Stripe fires `checkout.session.completed`\n(mode=payment) at the webhook; the ledger stays authoritative.","operationId":"topup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupReq"}}},"required":true},"responses":{"200":{"description":"Stripe Checkout URL for the credit pack","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlResp"}}}},"400":{"description":"Pack has no Stripe price"},"403":{"description":"Admin role required"},"404":{"description":"Credit pack not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/business-profile":{"get":{"tags":["admin"],"summary":"Get business profile","description":"Returns the workspace's business profile, including its configured domain, the most\nrecently crawled summary, brand palette, fonts, logo, crawl status and any last\nerror, plus company enrichment (industry, segment, size hint) and detected\ntechnology signals ordered by confidence. The response also lists which fields have\nbeen manually overridden by an admin, with overrides taking precedence over\nauto-discovered values. Readable by any signed-in workspace member.","operationId":"get_profile","responses":{"200":{"description":"Tenant business profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"put":{"tags":["admin"],"summary":"Update business profile","description":"Updates the workspace's business profile and returns the refreshed profile in the\nsame shape as the GET endpoint. Admin only. Each field may be set, cleared (to\nre-expose the auto-discovered value), or left untouched; manual overrides persist\nacross future automatic refreshes. Changing the domain to a new value normalizes it,\nrejects bare IP addresses with a 400, and triggers an immediate re-crawl.","operationId":"update_profile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutReq"}}},"required":true},"responses":{"200":{"description":"Updated profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResp"}}}},"400":{"description":"Invalid domain"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/business-profile/logo":{"post":{"tags":["admin"],"summary":"Upload business logo","description":"Uploads a business logo supplied inline as base64 and returns the refreshed business\nprofile. Admin only. Accepts PNG, JPEG, SVG, WebP and GIF up to 4 MiB; the stored\nlogo becomes a manual override of the auto-discovered logo and is served from a\nstable HQ-hosted URL.","operationId":"upload_logo","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoUploadReq"}}},"required":true},"responses":{"200":{"description":"Logo stored; returns the fresh profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResp"}}}},"400":{"description":"Invalid upload"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/business-profile/refresh":{"post":{"tags":["admin"],"summary":"Refresh business profile","description":"Queues an immediate re-crawl of the workspace's business profile and returns 202\nwith a queued status. Admin only. The refresh is skipped silently if the workspace\nhas not yet confirmed its domain.","operationId":"refresh_profile","responses":{"202":{"description":"Refresh queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/connect/status":{"get":{"tags":["integrations"],"summary":"Get connection status","description":"Returns the workspace's integration connection state and recommendations in one\npayload: whether Slack and Microsoft Teams are connected (with team/org name when\navailable), the browser-extension download and web-store URLs (null until\npublished), the list of suggested or accepted integration/skill recommendations\nenriched with catalog name, description, category, icon, confidence, priority, and\nrationale, and the technology signals detected about the organization's site and\nDNS. Scoped to the caller's workspace. Requires the agents:read scope.","operationId":"connect_status","responses":{"200":{"description":"Connection state + recommendations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectStatus"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/api/conversations":{"get":{"tags":["conversations"],"summary":"List conversations","description":"Returns a paginated list of the caller's conversations within their workspace, by\ndefault newest-activity first. Each row includes the conversation id, channel, mode,\nlast-activity time, title, a short preview of the latest message, the associated\nagent, an optional attached app, a delivered-artifact count, and, for email threads,\nfrom/to/subject. Supports a free-text query over title/agent/preview, sort (recent,\noldest, or title), a mode filter (fast vs standard, or all), and a channel filter\n(email is excluded by default unless explicitly requested). Workspace admins may\npass view=admin to list every conversation in the workspace; the response echoes the\nviewing mode, total match count, and page window.","operationId":"list_conversations","parameters":[{"name":"view","in":"query","description":"`?view=admin` opts into \"see every conv in tenant\", honored\nonly when the caller is `workspace_users.is_admin = true`.\nFalls back silently to participant view otherwise so a\ncrafted URL from a non-admin doesn't leak the existence of\nadmin mode via a 403.","required":false,"schema":{"type":["string","null"]}},{"name":"q","in":"query","description":"Free-text filter. Case-insensitive substring match over the\nconv title, the agent display name, and the latest-message\npreview (the three things a row shows). Empty/blank = no filter.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index. Clamped to >= 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Clamped to 1..=100; defaults to 25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest activity first), `oldest`, or\n`title` (A-Z, untitled convs sink to the end).","required":false,"schema":{"type":["string","null"]}},{"name":"mode","in":"query","description":"Which lane: `fast` lists only the no-VM \"ask\" threads; anything\nelse (default) lists the standard agent threads. The two lanes are\nsurfaced as separate tabs in Studio.","required":false,"schema":{"type":["string","null"]}},{"name":"channel_kind","in":"query","description":"Optional channel filter. When set to a known channel kind (e.g.\n`email`) the list returns ONLY threads of that kind - this backs the\nadmin Email > Threads tab. When unset, the default list EXCLUDES\n`email` so external-correspondence threads don't bleed into the main\nHome conversation list (they live under the Email section instead).\nUnknown values yield an empty set rather than an error.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Conversation list (paginated)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvListResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]},"post":{"tags":["conversations"],"summary":"Create a conversation","description":"Creates a new web conversation bound to a chosen agent and returns its identifiers\nso the client can navigate to the new thread and begin sending messages. Scoped to\nthe caller's workspace; the agent_id must belong to the caller's workspace\n(otherwise 404). The caller is added as the conversation owner. Returns 402 if the\nworkspace is out of credits, since the first turn would be refused.","operationId":"create_conversation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConvReq"}}},"required":true},"responses":{"200":{"description":"New web conversation created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConvResp"}}}},"402":{"description":"Workspace out of credits"},"404":{"description":"Agent not in tenant"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/conversations/{id}":{"get":{"tags":["conversations"],"summary":"Get a conversation","description":"Returns metadata for a single conversation the caller can access, including its\ntitle, mode (standard or fast), originating channel, surface type, whether it is\nread-only, the associated agent (id, slug, display name, avatar), and a directory of\nparticipants referenced in the thread. Scoped to the caller's own workspace and the\nconversations they participate in. Workspace admins may pass view=admin to read\nanother member's conversation, which is reflected in the returned viewing_as field.\nReturns 404 if the conversation is not found.","operationId":"get_conversation","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"view","in":"query","description":"`?view=admin` opts into \"see every conv in tenant\", honored\nonly when the caller is `workspace_users.is_admin = true`.\nFalls back silently to participant view otherwise so a\ncrafted URL from a non-admin doesn't leak the existence of\nadmin mode via a 403.","required":false,"schema":{"type":["string","null"]}},{"name":"q","in":"query","description":"Free-text filter. Case-insensitive substring match over the\nconv title, the agent display name, and the latest-message\npreview (the three things a row shows). Empty/blank = no filter.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index. Clamped to >= 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Clamped to 1..=100; defaults to 25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest activity first), `oldest`, or\n`title` (A-Z, untitled convs sink to the end).","required":false,"schema":{"type":["string","null"]}},{"name":"mode","in":"query","description":"Which lane: `fast` lists only the no-VM \"ask\" threads; anything\nelse (default) lists the standard agent threads. The two lanes are\nsurfaced as separate tabs in Studio.","required":false,"schema":{"type":["string","null"]}},{"name":"channel_kind","in":"query","description":"Optional channel filter. When set to a known channel kind (e.g.\n`email`) the list returns ONLY threads of that kind - this backs the\nadmin Email > Threads tab. When unset, the default list EXCLUDES\n`email` so external-correspondence threads don't bleed into the main\nHome conversation list (they live under the Email section instead).\nUnknown values yield an empty set rather than an error.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Conversation metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvMetaResp"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]},"delete":{"tags":["conversations"],"summary":"Delete a conversation","description":"Permanently deletes a conversation and cascades teardown of its backing app\nsurfaces, sandbox resources, messages, and related records. Scoped to the caller's\nworkspace and conversations they participate in. By default the conversation's\ndelivered artifacts are detached and kept (still downloadable by their existing\nlinks); passing delete_artifacts=true permanently deletes those artifacts instead.\nReturns the conversation id along with counts of apps destroyed and artifacts\ndeleted versus kept. Returns 404 if not found.","operationId":"delete_conversation","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"delete_artifacts","in":"query","description":"`true` also permanently deletes the conversation's delivered\nartifacts (blobs + rows). Default `false` - keep them (detach).","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Conversation deleted (cascade)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteConvResp"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]},"patch":{"tags":["conversations"],"summary":"Rename a conversation","description":"PATCH /v1/api/conversations/{id} - participant renames the\nconversation. A non-empty title is pinned (title_is_custom = true) so\nthe title.derive worker never overwrites it; an empty title clears\nboth so the conv falls back to auto-derivation. Admin-view callers\nare intentionally NOT allowed to rename someone else's conv (this is\na participant action), so we require default-view access.","operationId":"rename_conversation","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameConvReq"}}},"required":true},"responses":{"200":{"description":"Renamed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameConvResp"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/conversations/{id}/artifacts":{"get":{"tags":["conversations"],"summary":"List conversation artifacts","description":"Lists the delivered artifacts for a single conversation in chronological order.\nScoped to the caller's workspace and accessible conversations; workspace admins may\npass view=admin to view another member's conversation. Each entry includes the\nartifact id, name, content type, size, a sensitive flag, creation time, and a\ndownload URL. Returns only metadata; the bytes are fetched separately via each\nartifact's download URL.","operationId":"list_conversation_artifacts","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"view","in":"query","description":"`?view=admin` opts into \"see every conv in tenant\", honored\nonly when the caller is `workspace_users.is_admin = true`.\nFalls back silently to participant view otherwise so a\ncrafted URL from a non-admin doesn't leak the existence of\nadmin mode via a 403.","required":false,"schema":{"type":["string","null"]}},{"name":"q","in":"query","description":"Free-text filter. Case-insensitive substring match over the\nconv title, the agent display name, and the latest-message\npreview (the three things a row shows). Empty/blank = no filter.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index. Clamped to >= 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Clamped to 1..=100; defaults to 25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest activity first), `oldest`, or\n`title` (A-Z, untitled convs sink to the end).","required":false,"schema":{"type":["string","null"]}},{"name":"mode","in":"query","description":"Which lane: `fast` lists only the no-VM \"ask\" threads; anything\nelse (default) lists the standard agent threads. The two lanes are\nsurfaced as separate tabs in Studio.","required":false,"schema":{"type":["string","null"]}},{"name":"channel_kind","in":"query","description":"Optional channel filter. When set to a known channel kind (e.g.\n`email`) the list returns ONLY threads of that kind - this backs the\nadmin Email > Threads tab. When unset, the default list EXCLUDES\n`email` so external-correspondence threads don't bleed into the main\nHome conversation list (they live under the Email section instead).\nUnknown values yield an empty set rather than an error.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Delivered artifacts for the conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvArtifactsResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/conversations/{id}/artifacts/archive":{"get":{"tags":["conversations"],"summary":"Download conversation artifacts","description":"Streams a ZIP archive (application/zip) containing all of the conversation's\ndelivered artifacts. Scoped to the caller's workspace and conversations they\nparticipate in. Colliding filenames are de-duplicated within the archive. Returns\n404 when the conversation has no artifacts to download.","operationId":"download_conversation_artifacts","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"ZIP archive of the conversation's artifacts (application/zip)"},"404":{"description":"No artifacts to download"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/conversations/{id}/interrupt":{"post":{"tags":["conversations"],"summary":"Interrupt the active turn","description":"`POST /v1/api/conversations/{id}/interrupt` - the user-facing \"Stop\". Fires the\nin-flight turn's abort token: the turn's stream loop breaks, the RunTurn\ngRPC drops, the vsock to the in-VM adapter tears down, and the adapter hits\nEOF and interrupts its SDK - the agent actually halts (Claude-CLI Esc).\nIdempotent: no active turn for this conv -> 200 with `stopped: false`.","operationId":"interrupt","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Stop signal sent; `{ stopped }` is true iff a turn was in flight"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/conversations/{id}/messages":{"get":{"tags":["conversations"],"summary":"List conversation messages","description":"Returns the conversation's transcript as a sequence-ordered list of journal rows,\neach carrying its direction, kind, JSON payload, sequence number, turn id,\ntimestamp, and resolved author name/avatar; this is the cold-load companion to the\nlive event stream. Optional query parameters filter the results: since_seq returns\nonly events after a given sequence number (used to fill gaps on stream reconnect),\nturn_id restricts to a single turn, direction restricts to inbound or outbound, and\nview=admin lets a workspace admin read a conversation they do not participate in.\nThe caller must have access to the conversation, otherwise 404 is returned.","operationId":"list_messages","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"since_seq","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"turn_id","in":"query","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"direction","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"view","in":"query","description":"`?view=admin` opts a tenant admin into reading a journal\nthey don't participate in; non-admins are silently\ndowngraded. Same shape as conversation_meta. Honored as\nread-only - there's no admin-elevation on POST.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"The conversation transcript: journal rows, seq-ordered (the cold-load companion to the live SSE stream)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageRow"}}}}},"404":{"description":"No such conversation for the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"post":{"tags":["conversations"],"summary":"Post a message to a conversation","description":"`POST /v1/api/conversations/{id}/messages` -- Phase A.4 (#726).\n\nDecoupled from the SSE stream: returns `202 Accepted` after\nidempotency check, spawns the turn driver in the background,\ncaller observes events via `GET /v1/api/conversations/{id}/stream`\n(already attached or about to attach).\n\nIdempotency is decided IN THIS HANDLER, before spawning. The\nspawn passes `idempotency_key: None` so the inner pipeline's\n`prepare_turn` skips its own re-check -- avoids a double\nSETNX that would log a spurious \"duplicate\" on the fresh\npath.","operationId":"post_message","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitMessageBody"}}},"required":true},"responses":{"202":{"description":"Turn accepted (spawned in the background); attach to `stream_url` for events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitResponse"}}}},"404":{"description":"No such conversation for the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/conversations/{id}/nudge":{"post":{"tags":["conversations"],"summary":"Nudge a conversation","description":"Buffers an extra text message to be delivered to an agent turn that is currently in\nprogress on the conversation, returning whether it was buffered and how many nudges\nare now pending. The caller must be a participant of the conversation (404\notherwise), and there must be an active turn to nudge, otherwise the request returns\n409.","operationId":"nudge","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NudgeBody"}}},"required":true},"responses":{"200":{"description":"Nudge buffered for the in-flight turn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NudgeResp"}}}},"404":{"description":"No such conversation for the caller"},"409":{"description":"No active turn to nudge"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/conversations/{id}/stream":{"get":{"tags":["conversations"],"summary":"Stream conversation events","description":"Opens a Server-Sent Events stream of a conversation's live turn activity, emitting\nnamed events (agent token, tool, and final event kinds, plus stale when the client\nhas lagged and done when the turn closes) each carrying a JSON data payload and an\nincrementing id sequence. Resume after a disconnect with the standard Last-Event-ID\nheader or the since_seq query parameter; the optional scope parameter narrows the\ndetail level (and is clamped to the caller's token scope), and view=admin lets an\nadmin attach to a conversation they do not participate in. The caller must have\naccess to the conversation, otherwise 404 is returned.","operationId":"stream","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"scope","in":"query","description":"Self-downgrade only. `user` strips diagnostic + accounting\nfields; `admin` / `integrator` and absent default to the\ntrusted full vocabulary. Unknown values do NOT elevate -- see\n[`crate::stream_scope::Scope::from_query`]. Auth-derived\nscope replaces this in #729.","required":false,"schema":{"type":["string","null"]}},{"name":"since_seq","in":"query","description":"Explicit \"I've already seen up to seq N\" cursor for fresh\nattaches. The standard `EventSource` API doesn't let JS set\n`Last-Event-ID` on the initial request -- only on automatic\nreconnects -- so Studio passes its journal high-water mark\nhere to avoid a race where events emitted between the journal\nfetch and the SSE subscribe vanish into the gap.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"view","in":"query","description":"`?view=admin` opts a tenant admin into attaching to a\nconv stream they don't participate in. Non-admins are\nsilently downgraded. Audited per-attach so admin tail\nreads of someone else's conv are observable.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Server-Sent Events for the conversation's turn. Each frame is a named event (the agent token/tool/final event kinds, plus `stale` when the client lagged and `done` when the turn closes) carrying a JSON `data` payload and an `id:` sequence. Resume after a drop with the standard `Last-Event-ID` header, or pass `?since_seq=` on a fresh attach. Native EventSource (cookie auth) and fetch-stream (Bearer PAT) both work.","content":{"text/event-stream":{}}},"404":{"description":"No such conversation for the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/conversations/{id}/surfaces":{"get":{"tags":["conversations"],"summary":"List conversation surfaces","description":"Lists the live application surfaces (apps) attached to a conversation, excluding\ndestroyed ones, ordered by most recent activity. Scoped to the caller's workspace\nand accessible conversations; workspace admins may pass view=admin to inspect\nanother member's conversation. Each surface includes its id, slug, state, declared\nport, run mode, and authentication mode, plus a shareable visitor-facing URL and a\nseparate owner-only developer-embed URL (signed when required).","operationId":"list_surfaces","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"view","in":"query","description":"`?view=admin` opts into \"see every conv in tenant\", honored\nonly when the caller is `workspace_users.is_admin = true`.\nFalls back silently to participant view otherwise so a\ncrafted URL from a non-admin doesn't leak the existence of\nadmin mode via a 403.","required":false,"schema":{"type":["string","null"]}},{"name":"q","in":"query","description":"Free-text filter. Case-insensitive substring match over the\nconv title, the agent display name, and the latest-message\npreview (the three things a row shows). Empty/blank = no filter.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index. Clamped to >= 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Clamped to 1..=100; defaults to 25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest activity first), `oldest`, or\n`title` (A-Z, untitled convs sink to the end).","required":false,"schema":{"type":["string","null"]}},{"name":"mode","in":"query","description":"Which lane: `fast` lists only the no-VM \"ask\" threads; anything\nelse (default) lists the standard agent threads. The two lanes are\nsurfaced as separate tabs in Studio.","required":false,"schema":{"type":["string","null"]}},{"name":"channel_kind","in":"query","description":"Optional channel filter. When set to a known channel kind (e.g.\n`email`) the list returns ONLY threads of that kind - this backs the\nadmin Email > Threads tab. When unset, the default list EXCLUDES\n`email` so external-correspondence threads don't bleed into the main\nHome conversation list (they live under the Email section instead).\nUnknown values yield an empty set rather than an error.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Surfaces (apps) attached to the conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvSurfacesResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/documents":{"get":{"tags":["documents"],"summary":"List documents","description":"Returns the documents visible to the caller, newest first, along with the distinct\ncategories and tags across that visible set for building filter sidebars. Visibility\nis enforced per request: team-scoped documents are seen by everyone in the\nworkspace, private ones only by their owner, and channel-scoped ones only by members\nof that channel. Optional query parameters scope, category, channel, and tags\n(comma-separated, matching documents that carry all listed tags) narrow the results,\nand limit defaults to 50 and is capped at 200. Requires the documents:read scope.","operationId":"list_documents","parameters":[{"name":"scope","in":"query","description":"Filter to one scope; omit for \"everything I can see\".","required":false,"schema":{"type":["string","null"]}},{"name":"category","in":"query","description":"Filter to one category; omit for all.","required":false,"schema":{"type":["string","null"]}},{"name":"tags","in":"query","description":"Filter to docs that have ALL listed tags. Comma-separated.","required":false,"schema":{"type":["string","null"]}},{"name":"channel","in":"query","description":"Filter to one channel (only useful with `scope=channel`).","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"limit","in":"query","description":"Default 50, max 200.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Documents visible to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]},"post":{"tags":["documents"],"summary":"Upload a document","description":"Uploads a document by sending its bytes inline as base64 in the JSON body (capped at\nroughly 12 MiB of file content), together with filename, content type, scope\n(private, channel, or team; defaults to private), and optional channel_id, category,\ntags, and caption. The operation is content-addressed and idempotent per owner and\nscope: re-uploading identical bytes returns the existing document rather than\ncreating a duplicate, and any newly supplied tags, caption, or category are merged\nonto that existing row (reflected by deduplicated and enriched flags in the\nresponse). channel_id is required when scope is channel and rejected otherwise.\nReturns the document id, download URL, SHA-256, and size. Requires the\ndocuments:write scope.","operationId":"upload_document","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadReq"}}},"required":true},"responses":{"200":{"description":"Document saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResp"}}}},"400":{"description":"Invalid base64, invalid scope, or empty filename"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/documents/search":{"get":{"tags":["documents"],"summary":"Search documents","description":"Performs a case-insensitive substring search over the filename, caption, summary,\nand tags of documents the caller can see, returning matches newest first. Visibility\nrules are the same as the list endpoint (private, channel, and team scopes). Accepts\na required q query parameter and an optional limit (default 50, max 200); the\ncategories and tags rollup fields are returned empty for this endpoint. Requires the\ndocuments:read scope.","operationId":"search_documents","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Matching documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]}},"/v1/api/documents/upload":{"post":{"tags":["documents"],"summary":"Upload a document (multipart)","description":"Uploads a single document as multipart/form-data, intended for browser drag-and-drop\nand accepting larger files than the inline JSON path (up to 64 MiB). The file bytes\ngo in a part named file (or body), with optional text fields scope (private,\nchannel, or team; defaults to private), channel_id, category, tags\n(comma-separated), and caption. Behavior otherwise matches the inline upload,\nincluding content-addressed deduplication and metadata enrichment of an existing\nidentical document. Returns the document id, download URL, SHA-256, and size.\nRequires the documents:write scope.","operationId":"upload_multipart","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","description":"Documentation shape for the `POST /v1/api/documents/upload` multipart\nform. The handler reads the parts directly; this only drives the spec.","required":["file"],"properties":{"caption":{"type":["string","null"]},"category":{"type":["string","null"]},"channel_id":{"type":["string","null"],"description":"Required iff `scope=channel`."},"file":{"type":"string","format":"binary","description":"The file bytes (a file part carrying filename + content-type)."},"scope":{"type":["string","null"],"description":"`private` | `channel` | `team`. Defaults to `private`."},"tags":{"type":["string","null"],"description":"Comma-separated tag list."}}}}},"required":true},"responses":{"200":{"description":"Document saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResp"}}}},"400":{"description":"Missing file part / filename, invalid scope, or empty filename"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/documents/{id}":{"get":{"tags":["documents"],"summary":"Get a document","description":"Returns the full metadata for a single document by id, including its filename,\ncontent type, size, SHA-256 hash, scope, owner, category, tags, caption, summary,\nclassification status and confidence, detected language and document date, extracted\nreference numbers and entities, and a download URL. The document is only returned if\nit is visible to the caller under the private/channel/team visibility rules;\notherwise a 404 is returned. Requires the documents:read scope.","operationId":"get_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRow"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]},"delete":{"tags":["documents"],"summary":"Delete a document","description":"Deletes a document. Only the document's owner may delete it; other callers receive\n403 and an unknown document returns 404. The stored file content is removed only\nwhen no other document in the workspace references the same bytes, so deduplicated\ncopies are preserved. Returns 204 No Content on success. Requires the\ndocuments:write scope.","operationId":"delete_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"403":{"description":"Not the owner"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]},"patch":{"tags":["documents"],"summary":"Update a document","description":"Updates the editable metadata of a document: its scope, channel assignment,\ncategory, tags, and caption. Only fields present in the request body are changed;\nfor channel_id, category, and caption, an explicit null clears the value while\nomitting the field leaves it untouched. Only the document's owner may modify it\n(others receive 403, and a missing or non-visible document returns 404). Returns the\nupdated document. Requires the documents:write scope.","operationId":"update_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchReq"}}},"required":true},"responses":{"200":{"description":"Updated document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRow"}}}},"400":{"description":"Invalid scope"},"403":{"description":"Not the owner"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/documents/{id}/download":{"get":{"tags":["documents"],"summary":"Download a document","description":"Streams back the raw bytes of a document as a file attachment, setting the original\nfilename, content type, and content length, with a short private cache lifetime. The\ndocument is only served if it is visible to the caller under the same\nprivate/channel/team visibility rules as the read endpoints; otherwise a 404 is\nreturned. Requires the documents:read scope.","operationId":"download_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The document bytes (streamed attachment)"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]}},"/v1/api/documents/{id}/reclassify":{"post":{"tags":["documents"],"summary":"Reclassify a document","description":"Force the classifier worker to take another swing at this\ndocument. Owner-only. Resets `classification_status` to `queued`,\nzeros the rescue retry counter, and re-emits\n`hq.<tenant>.document.created` so a worker picks it up.\nUseful when the model returned a bad category, the upload arrived\nbefore the worker was up, or the doc terminal-failed and the user\nwants another go.","operationId":"reclassify","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Re-classification queued","content":{"application/json":{"schema":{}}}},"403":{"description":"Not the owner"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/email/activity":{"get":{"tags":["admin"],"summary":"List email activity","description":"Returns a paginated feed of inbound and outbound email messages decorated with their\nendpoint, plane and a deep-link to the conversation thread, along with the total\ncount for pagination. Admins see all workspace mail while a regular member sees only\nmail bound to them. Supports optional filtering by handling state (handled,\nawaiting_review, unrouted) and by endpoint, a limit (1-200, default 50), an offset,\nand a sort of recent (default, newest first) or oldest.","operationId":"list_activity","parameters":[{"name":"state","in":"query","description":"Optional attention-state filter (`handled` / `awaiting_review` /\n`unrouted`).","required":false,"schema":{"type":["string","null"]}},{"name":"endpoint_id","in":"query","description":"Optional endpoint filter.","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest first) or `oldest`.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Inbound + outbound email activity (scoped to the caller)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityListResp"}}}},"400":{"description":"Unknown state filter"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/email/badge":{"get":{"tags":["admin"],"summary":"Get email badge count","description":"Returns the navigation badge count of email items needing a human, broken down into\nawaiting-review, unrouted and quarantined plus their sum. Scoped to the caller: a\nnon-admin sees only their own awaiting-review items, while the unrouted and\nquarantined counts are admin-only and report zero for non-admins.","operationId":"get_badge","responses":{"200":{"description":"Nav-badge count of items needing a human","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Badge"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/email/endpoints":{"get":{"tags":["admin"],"summary":"List email endpoints","description":"Lists the workspace's inbound email endpoints (custom inboxes plus the HQ-managed\nworkspace mailbox), each with its configuration and per-endpoint counters: total\nmessages, items needing attention, inbound received, outbound replies,\nawaiting-review, unrouted and escalated counts, and a reply-language distribution.\nAlso returns the workspace email slug, email domain and outbound mailbox localpart\nso a full address can be previewed without another call. Admin only and scoped to\nthe caller's workspace.","operationId":"list_endpoints","responses":{"200":{"description":"Inbound email endpoints + counters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndpointListJson"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"post":{"tags":["admin"],"summary":"Create email endpoint","description":"Creates a new inbound email endpoint (custom inbox) and returns it with its\ncounters. Admin only. The localpart must be valid and not reserved or equal to the\nworkspace mailbox; 'agent' (plane=agent) endpoints require a handler agent belonging\nto the workspace, while 'light' quick-reply endpoints must not name an agent.\nReturns 402 if the plan's email-inbox limit is reached and 409 if the address (or an\nagent's existing endpoint) already exists.","operationId":"create_endpoint","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReq"}}},"required":true},"responses":{"200":{"description":"Endpoint created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndpointJson"}}}},"400":{"description":"Invalid localpart/kind/config, agent-mode mismatch, or reserved address"},"402":{"description":"Plan email-inbox limit reached"},"409":{"description":"Address already in use"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/endpoints/{id}":{"get":{"tags":["admin"],"summary":"Get email endpoint","description":"Returns a single inbound email endpoint by id, with its full configuration and\nper-endpoint counters (message totals, attention items, disposition counts and\nreply-language distribution). Admin only and scoped to the caller's workspace; an\nunknown or cross-workspace id returns 404.","operationId":"get_endpoint","parameters":[{"name":"id","in":"path","description":"Endpoint id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Endpoint detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndpointJson"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"delete":{"tags":["admin"],"summary":"Delete email endpoint","description":"Deletes an inbound email endpoint by id. Admin only and scoped to the caller's\nworkspace. The HQ-managed workspace mailbox cannot be deleted (400), and an unknown\nor cross-workspace id returns 404.","operationId":"delete_endpoint","parameters":[{"name":"id","in":"path","description":"Endpoint id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Endpoint deleted","content":{"application/json":{"schema":{}}}},"400":{"description":"Cannot delete the system workspace mailbox"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"patch":{"tags":["admin"],"summary":"Update email endpoint","description":"Updates the configurable behavior of an inbound email endpoint and returns the\nrefreshed endpoint with counters. Admin only and scoped to the caller's workspace.\nOmitted fields keep their current values, supplying allow_patterns replaces the\nallowlist wholesale, and the localpart and kind cannot be changed. The system\nworkspace mailbox cannot be edited here (400), and an unknown or cross-workspace id\nreturns 404.","operationId":"update_endpoint","parameters":[{"name":"id","in":"path","description":"Endpoint id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigPatch"}}},"required":true},"responses":{"200":{"description":"Endpoint updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndpointJson"}}}},"400":{"description":"Invalid config, or the system workspace mailbox can't be edited here"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/messages/{id}/assign":{"post":{"tags":["admin"],"summary":"Assign an unrouted email","description":"Assign a handler to an `unrouted` item and re-drive it. Admin-only. Sets the\nmessage's agent (and, when the mail hit a handler-less mailbox endpoint, the\nendpoint's agent too, so future mail routes), then re-emits the routed event\nvia the outbox so the worker drives a turn. The re-drive forces the agent/VM\npath (plane=NULL): a manually-assigned item is rare + deliberate, and the\nagent path avoids the light-plane's sender-bound disclosure ambiguity.","operationId":"assign_email","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignReq"}}},"required":true},"responses":{"200":{"description":"Handler assigned + item re-driven","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not an unrouted item, or unknown agent"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/messages/{id}/attachments/{attachment_id}/download":{"get":{"tags":["admin"],"summary":"Download an email attachment","description":"`GET /v1/api/email/messages/{id}/attachments/{attachment_id}/download` - the\nraw bytes of one attachment, for the admin viewer's inline preview + download\n(the read-half reuses the shared FilePreview renderer). Fetch-on-view from\nSentio (never stored). The message is tenant-scoped here, and Sentio scopes\nthe attachment to its message, so an attachment is reachable only through a\nmessage the caller's tenant owns. Admin-only.","operationId":"download_attachment","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"attachment_id","in":"path","description":"Attachment id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Raw attachment bytes (fetch-on-view from Sentio)"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/messages/{id}/content":{"get":{"tags":["admin"],"summary":"Get email message content","description":"`GET /v1/api/email/messages/{id}/content` - the full inbound email for the\ndetail view. Headers + decoded bodies are fetched on demand from Sentio (the\nraw EML is never stored); attachment metadata comes from the mirror, with a\nTrove `artifact_id` for download when the attachment was materialized.","operationId":"get_message","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Full inbound email (fetch-on-view from Sentio)","content":{"application/json":{"schema":{}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/messages/{id}/discard":{"post":{"tags":["admin"],"summary":"Discard a reply draft","description":"Discard an `awaiting_review` draft without sending: settle to `handled`.","operationId":"discard_draft","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Draft discarded; item settled to handled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not awaiting_review"},"404":{"description":"Not found or not yours"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/email/messages/{id}/reply":{"post":{"tags":["admin"],"summary":"Send an email reply","description":"Approve (and optionally edit) the drafted reply on an `awaiting_review`\nitem: send it through the same `email.reply` MCP path the agent uses, then\nsettle the item to `handled`. The body comes from the request (the UI\npre-fills it with the draft from the rendered transcript and lets a human\nedit before sending).","operationId":"send_reply","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyReq"}}},"required":true},"responses":{"200":{"description":"Reply sent; item settled to handled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not awaiting_review, empty text, or no handler agent / bound user"},"404":{"description":"Not found or not yours"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/email/quarantine/{id}/accept":{"post":{"tags":["admin"],"summary":"Accept a quarantined email","description":"Accepts a pending quarantined inbound email, marking it accepted, and returns an ok\nflag with an optional explanatory note. Admin only. Optionally binds the sender to a\nnamed workspace user (required to clear an unknown-sender quarantine) so their\nfuture mail routes; when the destination is now reachable the message is\nre-delivered so an agent acts on it immediately, otherwise it routes on the sender's\nnext mail. Returns 404 if no pending quarantine with that id exists.","operationId":"accept_quarantine","parameters":[{"name":"id","in":"path","description":"Quarantine id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptReq"}}},"required":true},"responses":{"200":{"description":"Quarantine accepted (sender optionally bound + re-delivered)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"bind_user_id is not a member of this workspace"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/quarantine/{id}/content":{"get":{"tags":["admin"],"summary":"Get quarantined email content","description":"`GET /v1/api/email/quarantine/{id}/content` - view a quarantined message\nbefore accept/reject. Same fetch-on-view of the raw EML; attachments aren't\nmaterialized until accept, so the list is empty here.","operationId":"get_quarantine","parameters":[{"name":"id","in":"path","description":"Quarantine id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Raw quarantined message (fetch-on-view from Sentio)","content":{"application/json":{"schema":{}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/quarantine/{id}/reject":{"post":{"tags":["admin"],"summary":"Reject a quarantined email","description":"Reject a quarantined mail: mark it `admin_reject` (terminal). Admin-only.","operationId":"reject_quarantine","parameters":[{"name":"id","in":"path","description":"Quarantine id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Quarantine rejected (terminal)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/email/review":{"get":{"tags":["admin"],"summary":"List emails awaiting review","description":"Returns the three actionable email triage queues - awaiting-review, unrouted and\nquarantined - each capped at 200 items, plus a count of each. The awaiting-review\nqueue is scoped to the caller (or all workspace mail for admins), while the unrouted\nand quarantined queues are admin-only and come back empty with zero counts for a\nnon-admin caller.","operationId":"list_review","responses":{"200":{"description":"The three actionable triage queues + counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewQueues"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/email/workspace-mailbox":{"patch":{"tags":["admin"],"summary":"Update workspace mailbox","description":"Changes the localpart of the workspace outbound mailbox, the address all agents send\nfrom, renaming both the workspace setting and its system endpoint together. Admin\nonly. The localpart must be lowercase alphanumerics with '.' or '-' separators (max\n63 chars) and is rejected if it is reserved or already used by a custom inbox; the\nmailbox never auto-replies and that behavior is not configurable here.","operationId":"update_mailbox","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMailboxPatch"}}},"required":true},"responses":{"200":{"description":"Workspace mailbox localpart updated","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid or reserved localpart"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/fast/{id}/promote":{"post":{"tags":["conversations"],"summary":"Promote a fast session","description":"Promotes a lightweight fast (quick-ask) conversation into a full standard\nconversation, binding it to the workspace's default agent so subsequent turns run in\nthe full agent environment seeded with the existing transcript. The caller must be a\nparticipant of the conversation (returns 403 otherwise). The promotion is one-way\nand idempotent: it is a no-op if the conversation is already standard. Requires the\nconversations:write scope.\nPromote a fast session to a full hq: flip `mode` off `fast`,\none-way. The conversation already holds its transcript, so the next\nturn in Studio cold-boots a VM seeded with that history via the normal\npath - no backfill. Idempotent: a no-op if it was already standard.","operationId":"promote","parameters":[{"name":"id","in":"path","description":"Fast conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Promoted to a standard conversation","content":{"application/json":{"schema":{}}}},"403":{"description":"Not a participant"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/inbox":{"get":{"tags":["notifications"],"summary":"List inbox messages","description":"Returns a paginated page of the caller's personal in-app inbox messages\n(agent-delivered notifications such as scheduled-task digests), with each message's\ntitle, body, source and read state, plus the total matching count, the\nworkspace-wide unread count for the nav badge, and the current page and page size.\nScoped to the caller's own inbox; admins do not see other users' inboxes. Supports\nan unread-only filter, a case-insensitive free-text search over title, body and\nschedule name, paging (page and page_size 1-100, default 25), and a sort of recent\n(default) or oldest.","operationId":"list_inbox","parameters":[{"name":"unread","in":"query","description":"`true` filters to read_at IS NULL.","required":false,"schema":{"type":["boolean","null"]}},{"name":"q","in":"query","description":"Free-text filter: case-insensitive substring over the message\ntitle, body, and (for schedule digests) the schedule name.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index. Clamped to >= 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page. Clamped to 1..=100; defaults to 25.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"sort","in":"query","description":"`recent` (default, newest delivery first) or `oldest`.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"Back-compat: an old SPA build calls `?limit=`. Used as the\npage_size fallback so the pre-pagination client still works.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"The caller's inbox page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/inbox/read_all":{"post":{"tags":["notifications"],"summary":"Mark all inbox messages read","description":"Marks all of the caller's unread inbox messages as read. Scoped to the caller's own\ninbox and idempotent (returns ok even when nothing was unread).","operationId":"mark_all_inbox_read","responses":{"200":{"description":"All marked read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/inbox/{id}":{"delete":{"tags":["notifications"],"summary":"Delete an inbox message","description":"Permanently deletes one of the caller's inbox messages. Scoped to the caller's own\ninbox; returns 404 if the message does not exist or is not theirs.","operationId":"delete_inbox","parameters":[{"name":"id","in":"path","description":"Inbox message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"404":{"description":"Message not found"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/inbox/{id}/read":{"post":{"tags":["notifications"],"summary":"Mark an inbox message read","description":"Marks one of the caller's inbox messages as read, preserving the original read\ntimestamp if it was already read. Scoped to the caller's own inbox; returns 404 if\nthe message does not exist or is not theirs.","operationId":"mark_inbox_read","parameters":[{"name":"id","in":"path","description":"Inbox message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"404":{"description":"Message not found"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/inbox/{id}/unread":{"post":{"tags":["notifications"],"summary":"Mark an inbox message unread","description":"Marks one of the caller's inbox messages as unread by clearing its read timestamp.\nScoped to the caller's own inbox; returns 404 if the message does not exist or is\nnot theirs.","operationId":"mark_inbox_unread","parameters":[{"name":"id","in":"path","description":"Inbox message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked unread","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"404":{"description":"Message not found"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/me":{"get":{"tags":["me"],"summary":"Get my profile","description":"Returns the signed-in user's self-view, scoped to the caller's own identity.\nIncludes an identity block (display name, email, locale, timezone, admin/bot flags,\nconnected-workspace source, profile-consent state, and clock/timezone display\npreferences), an optional persona block summarizing the user's distilled profile\n(null if not yet generated), quick counts such as the number of schedules owned, the\nworkspace onboarding state, and an out_of_credits flag indicating whether the\nworkspace has run out of credits.","operationId":"get_me","responses":{"200":{"description":"The signed-in user's self-view","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"patch":{"tags":["me"],"summary":"Update my profile","description":"`PATCH /v1/api/me` - update the caller's own profile preferences (clock\nformat + timezone). Self-scoped: the caller is resolved from the session\nheaders (the same pair `me` reads), so it can only ever write its own row.\nEither field may be omitted; only the supplied ones change.","operationId":"update_me","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchMeReq"}}},"required":true},"responses":{"200":{"description":"Preferences updated","content":{"application/json":{"schema":{}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/me/erase":{"post":{"tags":["me"],"summary":"Erase my own data","description":"`POST /v1/api/me/erase` - self-erasure. Any authenticated user can\nrequest this on their own data. No admin role required.","operationId":"erase_me","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EraseReq"}}},"required":true},"responses":{"200":{"description":"Erasure summary","content":{"application/json":{"schema":{}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/me/usage":{"get":{"tags":["billing"],"summary":"Get my billing usage","description":"Billing usage for the caller's own tenant (billing B1). Session-scoped\n(resolved from the workspace + user headers) and admin-gated - credits and\nspend are workspace-admin information. Reuses the same projection as the\noperator route `/v1/admin/usage`; see `crate::usage_admin::summarize`.","operationId":"my_usage","responses":{"200":{"description":"Workspace usage and billing summary","content":{"application/json":{"schema":{}}}},"403":{"description":"Workspace admin required"}},"security":[{"bearer_pat":["billing:read"]},{"oauth2":["billing:read"]}]}},"/v1/api/notifications":{"get":{"tags":["notifications"],"summary":"List notifications","description":"Returns the caller's in-app notification center: up to 100 non-dismissed\nnotifications (newest first), each with its kind, title, body, optional\ncall-to-action, severity and status, plus a count of how many are still unread for\nthe bell badge. Scoped to the caller's workspace and includes both notifications\naddressed to the caller and workspace-wide ones visible to any member.","operationId":"list_notifications","responses":{"200":{"description":"Notifications for the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationsResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/notifications/read-all":{"post":{"tags":["notifications"],"summary":"Mark all notifications read","description":"Marks all of the caller's unread notifications as read and returns the number\nupdated. Scoped to the caller's workspace and to notifications addressed to the\ncaller or workspace-wide.","operationId":"mark_all_notifications_read","responses":{"200":{"description":"All marked read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadAllResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/notifications/{id}/dismiss":{"post":{"tags":["notifications"],"summary":"Dismiss a notification","description":"Dismisses a single notification so it no longer appears in the notification list,\nrecording the dismiss time. Scoped to the caller's workspace and to notifications\naddressed to the caller or workspace-wide; always returns ok.","operationId":"dismiss_notification","parameters":[{"name":"id","in":"path","description":"Notification id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dismissed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/notifications/{id}/read":{"post":{"tags":["notifications"],"summary":"Mark a notification read","description":"Marks a single notification as read and records the read time. Scoped to the\ncaller's workspace and to notifications addressed to the caller or workspace-wide;\nonly affects a notification currently in the unread state and always returns ok.","operationId":"mark_notification_read","parameters":[{"name":"id","in":"path","description":"Notification id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/oauth/apps":{"get":{"tags":["tokens"],"summary":"List OAuth apps","description":"Lists the OAuth applications the authenticated caller has registered, scoped to the\ncaller's own active (non-disabled) apps. Each entry includes the client_id, name,\nclient_type (public for PKCE-only or confidential for clients with a secret),\nredirect_uris, requestable scopes, and creation timestamp, ordered newest first.","operationId":"list_oauth_apps","responses":{"200":{"description":"The caller's registered OAuth apps","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSummary"}}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"post":{"tags":["tokens"],"summary":"Create an OAuth app","description":"Registers a new OAuth application owned by the authenticated caller, validating the\nsupplied client metadata (redirect URIs must be https or http loopback with no\nfragment, scopes must be known capability scopes). Returns the created app including\nits client_id and, for confidential clients, a client_secret, plus a\nregistration_access_token used for later RFC 7592 management; the secret and\nregistration token are shown only once and cannot be retrieved again. Each account\nis limited in how many apps it may register.","operationId":"create_app","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMetadata"}}},"required":true},"responses":{"200":{"description":"The created app (secret + registration token shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApp"}}}},"400":{"description":"Invalid metadata or scope outside the vocabulary"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/oauth/apps/{client_id}":{"put":{"tags":["tokens"],"summary":"Update an OAuth app","description":"Updates the mutable metadata (name, redirect URIs, and requestable scopes) of an\nOAuth app owned by the authenticated caller, identified by client_id in the path.\nThe client type and secret lifecycle are fixed at registration and cannot be changed\nhere. New metadata is validated with the same rules as on creation. Returns the\nupdated app, or 404 if the caller does not own an active app with that client_id.","operationId":"update_app","parameters":[{"name":"client_id","in":"path","description":"OAuth client id to update","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMetadata"}}},"required":true},"responses":{"200":{"description":"The updated app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSummary"}}}},"400":{"description":"Invalid metadata or scope outside the vocabulary"},"404":{"description":"No such app owned by the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"delete":{"tags":["tokens"],"summary":"Delete an OAuth app","description":"Disables (deregisters) an OAuth app owned by the authenticated caller, identified by\nclient_id in the path. Once disabled the client can no longer be used and its issued\ntokens stop working. Returns a deleted flag, or 404 if the caller does not own an\nactive app with that client_id.","operationId":"delete_app","parameters":[{"name":"client_id","in":"path","description":"OAuth client id to disable","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedResp"}}}},"404":{"description":"No such app owned by the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/oauth/authorizations":{"get":{"tags":["tokens"],"summary":"List OAuth authorizations","description":"Lists the third-party OAuth applications the authenticated caller has authorized\n(consented to). Each entry includes the client_id, app name, client_type, an active\nflag indicating whether the app is still registered and enabled, the union of scopes\nthe user has granted it, and timestamps for when it was first authorized and last\nupdated. First-party apps such as the browser extension never appear here since they\nskip consent.","operationId":"list_authorizations","responses":{"200":{"description":"Third-party apps the caller has authorized","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Authorization"}}}}},"403":{"description":"Not authenticated"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/oauth/authorizations/{client_id}":{"delete":{"tags":["tokens"],"summary":"Revoke an OAuth authorization","description":"Revokes the authenticated caller's authorization for a third-party OAuth app,\nidentified by client_id in the path. This removes the consent and immediately\nrevokes all of that app's live access and refresh tokens for this user, so the app's\nnext API call fails and it must request authorization again. Returns a revoked flag\nand tokens_revoked count, or 404 if the caller has no authorization for that\nclient_id. Scoped strictly to the caller's own grant; it never affects other users\nor apps.","operationId":"revoke_authorization","parameters":[{"name":"client_id","in":"path","description":"OAuth client id to revoke access for","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokedResp"}}}},"404":{"description":"No such authorization for the caller"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/onboarding/complete":{"post":{"tags":["onboarding"],"summary":"Complete onboarding","description":"Marks the caller's workspace as having completed onboarding, which lifts the\nonboarding gate that otherwise blocks normal API access for a new workspace. The\noperation is idempotent: it only sets the completion timestamp if onboarding was not\nalready complete. Admin only (returns 403 otherwise). Returns a simple ok\nacknowledgement.","operationId":"complete_onboarding","responses":{"200":{"description":"Onboarding marked complete","content":{"application/json":{"schema":{}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/onboarding/domain":{"post":{"tags":["onboarding"],"summary":"Set onboarding domain","description":"Sets and confirms the workspace's business domain during onboarding. The submitted\nvalue is normalized to a bare hostname and validated for shape and live DNS\nresolution, rejecting typos, unresolvable domains, and private or internal addresses\nwith a 400; on success the domain is recorded as confirmed and a profile crawl of\nthat website is started. Admin only (returns 403 otherwise). Returns a simple ok\nacknowledgement.","operationId":"set_domain","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainReq"}}},"required":true},"responses":{"200":{"description":"Domain confirmed + crawl enqueued","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid or unresolvable domain"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/onboarding/domain/check":{"get":{"tags":["onboarding"],"summary":"Check a domain","description":"`GET /v1/api/onboarding/domain/check?domain=` - live pre-check for the wizard\nso a typo is caught before submit. Returns the normalized host + whether it\nhas a valid shape + whether it resolves.","operationId":"check_domain","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Domain shape + resolution check","content":{"application/json":{"schema":{}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/onboarding/status":{"get":{"tags":["onboarding"],"summary":"Get onboarding status","description":"Returns the current onboarding state for the caller's workspace: whether onboarding\nhas been completed, whether the business domain has been confirmed, the configured\nbusiness domain if any, and a wizard track value (slack, teams, or web) indicating\nwhich onboarding flow applies. Scoped to the caller's own workspace.","operationId":"onboarding_status","responses":{"200":{"description":"Onboarding status + wizard track","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/recommendations/{id}/accept":{"post":{"tags":["integrations"],"summary":"Accept a recommendation","description":"Marks the named recommendation as accepted and returns its new status. Scoped to the\ncaller's workspace, so an id from another workspace returns 404. Accepting currently\nrecords intent only and does not yet perform the underlying integration connect or\nskill enable. Requires the agents:write scope.","operationId":"accept_recommendation","parameters":[{"name":"id","in":"path","description":"Recommendation id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{}}}},"404":{"description":"Recommendation not found"}},"security":[{"bearer_pat":["agents:write"]},{"oauth2":["agents:write"]}]}},"/v1/api/recommendations/{id}/dismiss":{"post":{"tags":["integrations"],"summary":"Dismiss a recommendation","description":"Marks the named recommendation as dismissed and returns its new status. Scoped to\nthe caller's workspace, so an id from another workspace returns 404. Requires the\nagents:write scope.","operationId":"dismiss_recommendation","parameters":[{"name":"id","in":"path","description":"Recommendation id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dismissed","content":{"application/json":{"schema":{}}}},"404":{"description":"Recommendation not found"}},"security":[{"bearer_pat":["agents:write"]},{"oauth2":["agents:write"]}]}},"/v1/api/schedules":{"get":{"tags":["schedules"],"summary":"List schedules","description":"Lists scheduled automated runs along with a total count for pagination. By default\n(scope=mine) it returns only the caller's own schedules; scope=workspace returns all\nschedules in the workspace and is admin only (returns 403 otherwise), optionally\nnarrowed to one owner via owner_user_id. Results can be filtered by lifecycle state,\nand archived schedules are excluded unless include_archived is set or an explicit\nstate filter is given; limit defaults to 100 (max 500) and offset applies to the\nworkspace view. Each schedule includes its trigger, name, current state, fire and\nfailure counts, and last and next fire times. Requires the schedules:read scope.","operationId":"list_schedules","parameters":[{"name":"scope","in":"query","description":"`mine` (default) or `workspace` (admin only).","required":false,"schema":{"type":["string","null"]}},{"name":"state","in":"query","description":"Explicit lifecycle state filter - passes through verbatim. When\nset (e.g. `state=archived`), `include_archived` is ignored.","required":false,"schema":{"type":["string","null"]}},{"name":"include_archived","in":"query","description":"When no explicit `state` filter is supplied, default behaviour\nEXCLUDES archived rows so the \"your schedules\" view shows the\nworking set (active + paused + quarantined). Set\n`include_archived=true` for the audit history view.","required":false,"schema":{"type":"boolean"}},{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"offset","in":"query","description":"Row offset for the admin paginator. Ignored for `scope=mine`.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"owner_user_id","in":"query","description":"Restrict a `workspace`-scope listing to one owner (the admin\n\"filter by user\" dropdown). Ignored for `scope=mine` (which is\nalready pinned to the caller).","required":false,"schema":{"type":["string","null"],"format":"uuid"}}],"responses":{"200":{"description":"Schedules","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}},"400":{"description":"Invalid scope (must be 'mine' or 'workspace')"},"403":{"description":"scope=workspace requires admin"}},"security":[{"bearer_pat":["schedules:read"]},{"oauth2":["schedules:read"]}]},"post":{"tags":["schedules"],"summary":"Create a schedule","description":"Creates a new schedule that periodically runs an agent with a given prompt and\ndelivery target, according to the supplied trigger. If agent_id is omitted the\nworkspace's default enabled agent is used, and a non-existent agent_id returns 404.\nThe new schedule is owned by the caller. Returns 201 with the created schedule; if\nthe workspace plan's schedule limit has been reached the request fails with 402.\nRequires the schedules:write scope.","operationId":"create_schedule","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInput"}}},"required":true},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResp"}}}},"400":{"description":"No agent_id provided and no default agent configured"},"402":{"description":"Plan schedule limit reached"},"404":{"description":"Agent not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/schedules/owners":{"get":{"tags":["schedules"],"summary":"List schedule owners","description":"`GET /v1/api/schedules/owners` (admin) - distinct schedule owners in the\nworkspace with a count each. Powers the admin \"filter by user\" dropdown so\nit lists only users who actually have schedules (not every member), and\nstays bounded regardless of how many schedules exist.","operationId":"list_owners","responses":{"200":{"description":"Distinct schedule owners","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnersResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["schedules:read"]},{"oauth2":["schedules:read"]}]}},"/v1/api/schedules/{id}":{"get":{"tags":["schedules"],"summary":"Get a schedule","description":"Returns the full detail of a single schedule by id, including its name, description,\ntrigger, run arguments, current lifecycle state, fire and failure counts, last and\nnext fire times, and a link to the conversation from its most recent run. The\nschedule is only returned if the caller owns it or is an admin; otherwise a 404 is\nreturned. Requires the schedules:read scope.","operationId":"get_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Schedule detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleJson"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:read"]},{"oauth2":["schedules:read"]}]},"delete":{"tags":["schedules"],"summary":"Cancel a schedule","description":"Cancels a schedule and archives it, stopping all future runs while retaining its\nhistory. Only the schedule's owner or an admin may cancel it (others receive 403).\nReturns an ok acknowledgement. Requires the schedules:write scope.","operationId":"cancel_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled + archived","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]},"patch":{"tags":["schedules"],"summary":"Update a schedule","description":"Updates an existing schedule's name, description, trigger, or run arguments; only\nthe fields supplied are changed, and an explicit null description clears it while\nomitting it keeps the current value. Only the schedule's owner or an admin may\nupdate it (others receive 403). Returns an acknowledgement that includes the\nrecomputed next fire time when the trigger changed. Requires the schedules:write\nscope.","operationId":"update_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInput"}}},"required":true},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"400":{"description":"Invalid args or trigger"},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/schedules/{id}/erase":{"delete":{"tags":["schedules"],"summary":"Erase an archived schedule","description":"Permanently erase an archived schedule. Owner or admin only;\nengine refuses to erase non-archived rows so the operator must\nhit the Cancel button first. Past `hq_flow.instances` rows\nsurvive with `schedule_id = NULL` (ON DELETE SET NULL on the FK).\n\nFuture hardening: emit an audit-log entry for the right-to-\nerasure trail, and gate workspace-wide schedules behind an\nextra admin-role check. For now, the same owner-or-admin gate\nas cancel applies - anyone who can cancel can also erase, but\nonly after archive.","operationId":"erase_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Permanently erased","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"400":{"description":"Schedule is not archived; cancel it first"},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/schedules/{id}/fire_now":{"post":{"tags":["schedules"],"summary":"Fire a schedule now","description":"Triggers an immediate run of the schedule without waiting for its next scheduled\ntime, leaving the regular firing cadence unchanged. Only the schedule's owner or an\nadmin may fire it (others receive 403). Returns an ok acknowledgement. Requires the\nschedules:write scope.","operationId":"fire_now","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Fired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"400":{"description":"Schedule is archived; cannot fire"},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/schedules/{id}/pause":{"post":{"tags":["schedules"],"summary":"Pause a schedule","description":"Pauses an active schedule so it stops firing until resumed. Only the schedule's\nowner or an admin may pause it (others receive 403). Returns an ok acknowledgement.\nRequires the schedules:write scope.","operationId":"pause_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Paused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/schedules/{id}/resume":{"post":{"tags":["schedules"],"summary":"Resume a schedule","description":"Resumes a previously paused schedule so it begins firing again, returning the newly\ncomputed next fire time when the schedule was successfully resumed. Only the\nschedule's owner or an admin may resume it (others receive 403). Requires the\nschedules:write scope.","operationId":"resume_schedule","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Resumed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResp"}}}},"400":{"description":"Schedule is archived or quarantined (update its trigger first)"},"403":{"description":"Not your schedule"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules:write"]}]}},"/v1/api/settings":{"get":{"tags":["admin"],"summary":"Get workspace settings","description":"Returns the workspace-level settings an admin controls. Admin only. Includes the\nconnected source's id, kind, and display name (read-only), the persona-distillation\nmode and per-user consent mode, the corpus history window in days, the sign-in mode\nand whether guest sign-in is allowed, and proactive-nudge settings (enabled flag,\ntimezone, start/end hours, and fallback language). Returns 403 for non-admins.","operationId":"get_settings","responses":{"200":{"description":"Workspace settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettingsResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"patch":{"tags":["admin"],"summary":"Update workspace settings","description":"Updates the workspace-level settings an admin controls and returns the full updated\nsettings. Admin only. Any subset of fields may be supplied; omitted fields are left\nunchanged, and for the nudge timezone and fallback language an empty string reverts\nto the default. Validates allowed values and ranges (for example profile_mode must\nbe enabled or disabled, consent_mode admin_only or per_user, corpus_history_days\n30-365, sign-in mode open or approval, and nudge start hour before end hour),\nreturning 400 on an invalid value and 403 for non-admins.","operationId":"update_settings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchReq"}}},"required":true},"responses":{"200":{"description":"Updated settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettingsResp"}}}},"400":{"description":"Invalid field value"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/slack/channels":{"get":{"tags":["integrations"],"summary":"List Slack channels","description":"Lists the Slack channels the HQ bot is a member of (each with id, name, and whether\nit is private), which is also the set of channels HQ can post to, for use in pickers\nsuch as the schedule editor. The response includes a connected flag that is false\nwhen the workspace has no active Slack installation, in which case the channel list\nis empty. Scoped to the caller's workspace; requires the agents:read scope.","operationId":"list_channels","responses":{"200":{"description":"Channels the HQ bot belongs to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelsResp"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/api/slack/channels/joinable":{"get":{"tags":["integrations"],"summary":"List joinable Slack channels","description":"`GET /v1/api/slack/channels/joinable` (admin). Public channels HQ is\nNOT a member of - the ones the admin can self-join with one click.\nPrivate channels are intentionally excluded: Slack forbids a bot from\nself-joining them, so they need an `/invite @HQ` from a member.","operationId":"list_joinable_channels","responses":{"200":{"description":"Public channels HQ can self-join","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelsResp"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/slack/channels/{id}/join":{"post":{"tags":["integrations"],"summary":"Join a Slack channel","description":"`POST /v1/api/slack/channels/{id}/join` (admin). Self-joins HQ to a\npublic channel via `conversations.join`. Idempotent on Slack's side\n(joining a channel you're already in just succeeds).","operationId":"join_channel","parameters":[{"name":"id","in":"path","description":"Slack channel id to join","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Joined","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinResp"}}}},"400":{"description":"No live Slack install, or Slack refused the join"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/surfaces/{id}":{"delete":{"tags":["conversations"],"summary":"Delete a surface","description":"Deletes an app surface, stopping it and releasing its resources. Scoped to the\ncaller's workspace; a workspace admin may delete any app in the workspace, while a\nnon-admin must participate in the surface's backing conversation. Returns the\nsurface id and the deletion outcome. Returns 404 if the surface is not found in the\ncaller's workspace.","operationId":"delete_surface","parameters":[{"name":"id","in":"path","description":"Surface id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"App (surface) deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicDeleteResp"}}}},"404":{"description":"Surface not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/surfaces/{id}/restore":{"post":{"tags":["conversations"],"summary":"Restore a surface","description":"Initiates restoration of a previously paused or archived app surface so it can run\nagain. Scoped to the caller's workspace, and the caller must be able to access the\nsurface's backing conversation. Returns the surface id and the restore outcome.\nReturns 404 if the surface is not found in the caller's workspace.","operationId":"restore_surface","parameters":[{"name":"id","in":"path","description":"Surface id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Surface restore initiated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicRestoreResp"}}}},"404":{"description":"Surface not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/surfaces/{id}/share":{"post":{"tags":["conversations"],"summary":"Share a surface","description":"Mints a time-limited signed share URL for an app surface that the caller can hand\nout, without making the surface publicly visible. Scoped to the caller's workspace,\nand the caller must be able to access the surface's backing conversation. The\noptional ttl_secs sets the validity window, defaulting to 7 days and constrained to\nbetween 1 minute and 30 days (out-of-range values are rejected). Returns the surface\nid, the tokenized URL, and its expiry as a Unix timestamp. Returns 404 if the\nsurface is not found.","operationId":"share_surface","parameters":[{"name":"id","in":"path","description":"Surface id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareReq"}}},"required":true},"responses":{"200":{"description":"Time-limited signed share URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResp"}}}},"404":{"description":"Surface not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/surfaces/{id}/visibility":{"post":{"tags":["conversations"],"summary":"Set surface visibility","description":"Sets an app surface's visibility to either private or public, controlling whether\nits bare URL works for anyone. Scoped to the caller's workspace, and the caller must\nbe able to access the surface's backing conversation. Rejects values other than\nprivate or public with 400. Returns the surface id, the resulting visibility, and\nthe underlying auth mode. Returns 404 if the surface is not found or is in a\nterminal state.","operationId":"set_visibility","parameters":[{"name":"id","in":"path","description":"Surface id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisibilityReqPublic"}}},"required":true},"responses":{"200":{"description":"Visibility updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisibilityRespPublic"}}}},"404":{"description":"Surface not found or terminal"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/tokens":{"get":{"tags":["tokens"],"summary":"List API tokens","description":"Lists the authenticated caller's own live personal access tokens. Scoped to the\ncaller's manually created tokens that are not revoked and not expired; OAuth-issued\naccess and refresh tokens (those tied to a client) are deliberately excluded. Each\nentry includes the token id, name, kind, granted scopes, and creation, last-used,\nand expiry timestamps, ordered newest first.","operationId":"list_tokens","responses":{"200":{"description":"The caller's live tokens","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TokenView"}}}}},"403":{"description":"Not authenticated"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"post":{"tags":["tokens"],"summary":"Create an API token","description":"Creates a new personal access token for the authenticated caller, returning the\nplaintext token exactly once (it cannot be retrieved again) along with its id, name,\nscopes, and optional expiry. Accepts a name, an optional list of capability scopes\n(resource:action, e.g. documents:read; omitting scopes defaults to full non-admin\naccess), and an optional expires_in_days (omitted means non-expiring). Each scope\nmust be a known capability or legacy tier, and requesting the admin scope requires\nthe caller to have the admin role. The token's effective permissions are always\nclamped to the caller's role at request time.","operationId":"create_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenReq"}}},"required":true},"responses":{"200":{"description":"The new token (plaintext shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenResp"}}}},"400":{"description":"Invalid name or scopes"},"403":{"description":"Scope requires the admin role"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/tokens/scopes":{"get":{"tags":["tokens"],"summary":"List token scopes","description":"Returns the vocabulary of resource:action capability scopes a personal access token\ncan be minted with. Each entry lists the scope string, a human-readable description\nof what it allows, and a requires_admin flag indicating whether granting it requires\nthe admin role. Requires authentication.","operationId":"list_scopes","responses":{"200":{"description":"The resource:action scope vocabulary a token can be minted with","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScopeOption"}}}}},"403":{"description":"Not authenticated"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/tokens/{id}":{"delete":{"tags":["tokens"],"summary":"Revoke an API token","description":"Revokes one of the authenticated caller's own personal access tokens, identified by\nits id in the path. The revocation takes effect immediately. Scoped to the caller's\nown tokens, so it cannot revoke another user's token. Returns a revoked flag, or 404\nif no matching live token belongs to the caller.","operationId":"revoke_token","parameters":[{"name":"id","in":"path","description":"Token id to revoke","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeResp"}}}},"404":{"description":"Token not found"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/users":{"get":{"tags":["admin"],"summary":"List users","description":"Returns the workspace's user directory as a paginated list, each entry including\nexternal id, source connector and workspace, display and real name, email, title,\ntimezone, locale, admin/bot/guest/deleted flags, profile-consent state, last-seen\ntime, sign-in access, admin-role override, and origin (slack, teams, or email).\nSupports a case-insensitive q search over name/handle/email/id, page and page_size\npagination (page_size clamped 1-200, default 50), and a status filter of active\n(default), deleted, or all. Admin only; bots are excluded and results are scoped to\nthe caller's own workspace.","operationId":"list_users","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring over real name / handle / email / external id.","required":false,"schema":{"type":["string","null"]}},{"name":"page","in":"query","description":"1-based page index (clamped to >= 1).","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"page_size","in":"query","description":"Rows per page (clamped 1..=200, default 50).","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"status","in":"query","description":"`active` (default, not erased), `deleted` (erased only), or `all`.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Workspace user directory","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResp"}}}},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"post":{"tags":["admin"],"summary":"Add a user by email","description":"`POST /v1/api/users` - add a member by email (no Slack/Teams needed). Creates\n(or re-activates) a `workspace_users` row on the tenant's synthetic `direct`\nsource with `origin='email'`, `signin_access='allowed'`, then mints a\nmagic-link invite. Returns the link when no transactional email sender is\nwired (so an admin can hand it over).","operationId":"add_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUserReq"}}},"required":true},"responses":{"200":{"description":"User added / invited","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid email or admin_access"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/users/{id}":{"get":{"tags":["admin"],"summary":"Get a user","description":"`GET /v1/api/users/{id}` - one user (admin-only, tenant-scoped) for the\ndetail page. Reuses USER_SELECT; bots/deleted are NOT excluded here (a deep\nlink to an erased user must still resolve so the detail can show its state).","operationId":"get_user","parameters":[{"name":"id","in":"path","description":"User id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"One user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRow"}}}},"404":{"description":"User not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"delete":{"tags":["admin"],"summary":"Revoke a pending invite","description":"`DELETE /v1/api/users/{id}` - revoke a PENDING email invite (added by email,\nnot yet signed in). Hard-deletes the row (no data to keep). Active members\n(signed in, or Slack/Teams) are NOT deletable here - that's the erasure flow.\nAdmin-only, tenant-scoped.","operationId":"revoke_invite","parameters":[{"name":"id","in":"path","description":"User id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Pending invite revoked","content":{"application/json":{"schema":{}}}},"400":{"description":"Only a pending email invite can be revoked"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"patch":{"tags":["admin"],"summary":"Update a user's access","description":"`PATCH /v1/api/users/{id}` - set one user's sign-in access and/or\ntenant-admin role. Admin-only, tenant-scoped: the UPDATE is joined through\nthe tenant slug so an admin can only ever touch a user in their OWN\nworkspace. Either field may be omitted (only the supplied ones change).","operationId":"update_user","parameters":[{"name":"id","in":"path","description":"User id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUserReq"}}},"required":true},"responses":{"200":{"description":"Access updated","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid field value"},"404":{"description":"User not found"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/api/users/{id}/resend":{"post":{"tags":["admin"],"summary":"Resend a user invite","description":"`POST /v1/api/users/{id}/resend` - re-send the magic-link invite to a pending\nemail member. Admin-only, tenant-scoped.","operationId":"resend_invite","parameters":[{"name":"id","in":"path","description":"User id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invite re-sent","content":{"application/json":{"schema":{}}}},"404":{"description":"No pending email invite for this user"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/catalog":{"get":{"tags":["integrations"],"summary":"Get the MCP catalog","description":"Returns the full catalog of available MCP integration servers along with the calling\nworkspace's install state for each. For every active server it reports static\nmetadata (display name, description, category, publisher, transport, trust tier,\nversion, credential model, and any featured ranking) plus per-tenant fields: whether\nthe tenant explicitly enabled or disabled it, the effective enabled state, the\ninstall status (active, paused, failed, or reauth_required), the install scopes the\noperator allows, and an icon URL when one exists. Admin only; scoped to the caller's\nown workspace.","operationId":"mcp_catalog","responses":{"200":{"description":"MCP registry + this tenant's install state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResp"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/catalog/{slug}/icon":{"get":{"tags":["integrations"],"summary":"Get an MCP catalog icon","description":"Returns the SVG brand icon for the given MCP server as image/svg+xml. The response\nis public, immutable, and long-cached; a 404 is returned when the server has no icon\nor is not active.","operationId":"mcp_icon","parameters":[{"name":"slug","in":"path","description":"MCP server slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The server's SVG icon (public, immutable, long-cached)"},"404":{"description":"No icon for this server"}}}},"/v1/mcp/installs":{"post":{"tags":["integrations"],"summary":"Install an MCP server","description":"Installs or re-enables an MCP integration for the caller's workspace, marking it\nactive and enabled. Servers using a tenant credential model (tenant_api_key or\ntenant_oauth) require a credential reference in the request, which must be\nprovisioned beforehand. Accepts an optional per-tenant config blob. Idempotent:\nre-installing updates the existing entry and preserves the prior credential\nreference and config when they are omitted. Returns the slug and enabled flag. Admin\nonly; scoped to the caller's own workspace.","operationId":"install_mcp","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallReq"}}},"required":true},"responses":{"200":{"description":"The installed / re-enabled server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResp"}}}},"400":{"description":"Credentialed server needs a credential_ref"},"404":{"description":"No active server with that slug"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/installs/{slug}":{"delete":{"tags":["integrations"],"summary":"Uninstall an MCP server","description":"Disables an MCP integration for the caller's workspace, setting it to not enabled\n(this works even for servers enabled by default with no prior explicit install).\nAlso clears any stored tenant credential for that server so a later re-install must\nsupply a fresh one. Returns the slug with enabled set to false. Admin only; scoped\nto the caller's own workspace.","operationId":"uninstall_mcp","parameters":[{"name":"slug","in":"path","description":"MCP server slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The now-disabled server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResp"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/installs/{slug}/api_key":{"post":{"tags":["integrations"],"summary":"Set an MCP server API key","description":"Stores an API key credential for an MCP server and installs/enables it in one call,\nfor servers whose credential model is tenant_api_key only. The scope may be team\n(shared across the workspace, the default) or user (private to the caller); the\nchosen scope must be among the server's allowed scopes, and user scope requires a\nresolvable calling user. The supplied key is securely sealed and, where the backend\nsupports it, validated against the upstream before being saved; an invalid key is\nrejected. Idempotent: pasting a new key for an existing install rotates the\ncredential in place. Returns the slug, enabled flag, the stored credential reference\nid, and the confirmed scope. Admin only; scoped to the caller's own workspace.","operationId":"set_mcp_key","parameters":[{"name":"slug","in":"path","description":"MCP server slug","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallApiKeyReq"}}},"required":true},"responses":{"200":{"description":"The server, installed with a sealed API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallApiKeyResp"}}}},"400":{"description":"Empty key, wrong credential_model, bad scope, or failed probe"},"404":{"description":"No active server with that slug"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/memory/about-me":{"get":{"tags":["memory"],"summary":"List facts about me","description":"Returns the facts the assistant currently remembers about the calling user, scoped\nto the caller's own account. Each fact includes its id, text, type, confidence, the\nagent that asserted it, when it was asserted, and the source conversation id. The\nresponse also includes a distilled one-paragraph persona summary (and when it was\nlast generated, if available) and the distinct fact types present across the full\nset for use as filters. Supports filtering by agent slug and fact type, page sizing\nvia limit (default 25, max 200), and forward pagination via an opaque cursor that\nreturns null once the end is reached.","operationId":"list_facts","parameters":[{"name":"agent","in":"query","description":"Restrict to one agent's view. Slug; omit for all agents in the\nworkspace that have facts about the caller.","required":false,"schema":{"type":["string","null"]}},{"name":"fact_type","in":"query","description":"Restrict to one fact_type. Omit for all.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"Default 25, max 200. Paginate via `cursor` rather than bumping\nthis for \"show more\" - limit is meant to size each page.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"cursor","in":"query","description":"Opaque cursor returned by the previous page. URL-safe base64\nof `{asserted_at, id}` - fed back as the WHERE bound so\nconcurrent inserts don't shift the page boundary.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Facts the assistant remembers about you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AboutMeResp"}}}}},"security":[{"bearer_pat":["memory:read"]},{"oauth2":["memory:read"]}]}},"/v1/memory/about-me/{fact_id}":{"delete":{"tags":["memory"],"summary":"Forget a fact about me","description":"Forgets one of the assistant's remembered facts about the calling user, marking it\nsuperseded with no replacement so it is no longer surfaced. Idempotent: returns a\nstatus of \"forgotten\" when the fact was active, or \"already_superseded_or_unknown\"\nwhen it was already gone or never existed (no error in that case). Scoped to the\ncaller's own account.","operationId":"forget_fact","parameters":[{"name":"fact_id","in":"path","description":"Fact id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Fact forgotten (or already gone)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgetResp"}}}}},"security":[{"bearer_pat":["memory:write"]},{"oauth2":["memory:write"]}]}},"/v1/memory/about-me/{fact_id}/correct":{"post":{"tags":["memory"],"summary":"Correct a fact about me","description":"Corrects one of the assistant's remembered facts about the calling user by\nsuperseding it with a user-supplied replacement (1 to 4096 characters), preserving\nthe original fact's type, confidence, agent, and source. The fact must currently be\nactive and about the caller. Returns the id of the superseded fact and the id of the\nnewly created corrected fact. Scoped to the caller's own account.","operationId":"correct_fact","parameters":[{"name":"fact_id","in":"path","description":"Fact id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorrectInput"}}},"required":true},"responses":{"200":{"description":"Fact superseded with your correction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorrectResp"}}}},"404":{"description":"No active fact about you with that id"}},"security":[{"bearer_pat":["memory:write"]},{"oauth2":["memory:write"]}]}},"/v1/memory/agents":{"get":{"tags":["memory"],"summary":"List freezable agents","description":"Returns every enabled agent in the caller's workspace alongside the calling user's\nown freeze state for each: whether it is frozen, when it was frozen, and the reason.\nIntended to drive a per-user \"agents that can read your memory\" toggle list. Scoped\nto the caller's own account.","operationId":"list_freezable_agents","responses":{"200":{"description":"Agents in the workspace + your freeze state for each","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezableAgentsResp"}}}}},"security":[{"bearer_pat":["memory:read"]},{"oauth2":["memory:read"]}]}},"/v1/memory/freeze":{"get":{"tags":["memory"],"summary":"List memory freezes","description":"Returns the list of agents the calling user has frozen out of their memory, each\nwith the agent slug, the optional reason given, and when the freeze was created,\nordered most recent first. Scoped to the caller's own account.","operationId":"list_freezes","responses":{"200":{"description":"Agents you've frozen out of your memory","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezeListResp"}}}}},"security":[{"bearer_pat":["memory:read"]},{"oauth2":["memory:read"]}]}},"/v1/memory/freeze/{agent_slug}":{"post":{"tags":["memory"],"summary":"Freeze an agent's memory","description":"Freezes the named agent out of the calling user's memory, revoking that agent's\nability to read facts about the caller. Accepts an optional reason. Idempotent:\nre-freezing updates the stored reason and timestamp. Returns the agent slug with\nfrozen set to true. Scoped to the caller's own account.","operationId":"freeze","parameters":[{"name":"agent_slug","in":"path","description":"Agent slug","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezeBody"}}},"required":true},"responses":{"200":{"description":"Agent frozen out of your memory","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezeResp"}}}}},"security":[{"bearer_pat":["memory:write"]},{"oauth2":["memory:write"]}]},"delete":{"tags":["memory"],"summary":"Unfreeze an agent's memory","description":"Unfreezes the named agent for the calling user, re-granting that agent's ability to\nread facts about the caller. Idempotent. Returns the agent slug with frozen set to\nfalse. Scoped to the caller's own account.","operationId":"unfreeze","parameters":[{"name":"agent_slug","in":"path","description":"Agent slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent un-frozen (re-granted read)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreezeResp"}}}}},"security":[{"bearer_pat":["memory:write"]},{"oauth2":["memory:write"]}]}},"/v1/models":{"get":{"tags":["agents"],"summary":"List models","description":"Lists the models that can be selected in the agent editor, each with its slug,\ndisplay name, family, runtime profile, vendor, and whether it is the runtime's\ndefault. Pass the optional runtime query parameter (claude-sdk, openai-agents, or\nopencode) to restrict the list to one runtime profile; otherwise all selectable\nmodels are returned. Requires the agents:read scope.","operationId":"list_models","parameters":[{"name":"runtime","in":"query","description":"Filter to one runtime profile (claude-sdk | openai-agents | opencode).","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Selectable models for the agent editor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelsResp"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/oauth/authorize":{"get":{"tags":["auth"],"summary":"OAuth authorization endpoint","description":"Starts an OAuth 2.1 authorization-code flow with PKCE, where HQ acts as the\nauthorization server. Requires an active HQ browser session; if the caller is not\nsigned in they are redirected to HQ sign-in and back. Validates the client_id\nagainst the client registry, checks that redirect_uri is on the client's allowlist,\nrequires code_challenge_method=S256, and grants the requested scopes intersected\nwith the client's allowed scopes (an empty scope param grants all allowed scopes;\nrequesting only disallowed scopes returns invalid_scope). On success it issues a\nsingle-use authorization code and redirects to the client's redirect_uri with code\nand state query parameters; third-party (non-first-party) clients are first sent to\na consent screen unless prior consent already covers the requested scopes.","operationId":"oauth_authorize","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"code_challenge","in":"query","required":true,"schema":{"type":"string"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"state","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"scope","in":"query","required":false,"schema":{"type":["string","null"]}}],"responses":{"302":{"description":"Redirect to the client's redirect_uri with ?code=&state= (third-party clients hit the consent screen first)"},"400":{"description":"invalid_request / unknown client_id / invalid_scope"},"429":{"description":"Too many requests (per-IP rate limit)"}}}},"/v1/oauth/introspect":{"post":{"tags":["auth"],"summary":"Introspect a token","description":"RFC 7662 token introspection: the calling client authenticates (client_id, plus\nclient_secret for confidential clients) and submits a token to check, as form or\nJSON. Returns active true only for a token that the authenticated client itself\nissued and that is neither revoked nor expired, along with its scope, client_id,\nusername, token_type, exp, iat, and sub; for any other token (unknown, expired,\nrevoked, or belonging to a different client) it returns active false without leaking\ndetails.","operationId":"oauth_introspect","requestBody":{"description":"RFC 7662. The calling client authenticates (client_id + secret for confidential). Form or JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectReq"}}},"required":true},"responses":{"200":{"description":"Token metadata for a token this client issued, or { active: false }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectResp"}}}},"401":{"description":"Invalid client credentials"},"429":{"description":"Too many requests (per-IP rate limit)"}}}},"/v1/oauth/register":{"post":{"tags":["auth"],"summary":"Register an OAuth client","description":"OAuth 2.1 Dynamic Client Registration (RFC 7591). Registration is authenticated: the\ncaller must present a valid HQ session or token, and the resulting client is owned\nby that user. Validates the submitted JSON metadata (redirect URIs must be https or\nhttp loopback with no fragment; scopes must be known capability scopes;\ntoken_endpoint_auth_method none yields a public client, client_secret_post/basic a\nconfidential one). Returns 201 with the new client_id, a registration_access_token,\nand, for confidential clients, a client_secret (the secret and registration token\nare shown only once), plus a registration_client_uri for managing the client.\nRegistered clients are always treated as third-party and shown the consent screen,\nand each account is capped on how many it may register.","operationId":"register_client","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMetadata"}}},"required":true},"responses":{"201":{"description":"Registered client (client_id + one-time secret/registration token), RFC 7591","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientInfo"}}}},"400":{"description":"invalid_redirect_uri / invalid_scope / invalid_client_metadata"},"401":{"description":"registration requires an authenticated HQ session or token"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/oauth/register/{client_id}":{"get":{"tags":["auth"],"summary":"Get an OAuth client","description":"RFC 7592 read of a dynamically registered OAuth client identified by client_id in\nthe path. Authenticated with the client's registration_access_token as a Bearer\ntoken (not an HQ session). Returns the client's current metadata including redirect\nURIs, name, token_endpoint_auth_method, grant and response types, and scopes. The\nclient_secret is never returned here.","operationId":"get_client","parameters":[{"name":"client_id","in":"path","description":"Client id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Client metadata (RFC 7592)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientInfo"}}}},"401":{"description":"Invalid registration_access_token"},"404":{"description":"Unknown client"}}},"put":{"tags":["auth"],"summary":"Update an OAuth client","description":"RFC 7592 update of a dynamically registered OAuth client identified by client_id in\nthe path, authenticated with the client's registration_access_token as a Bearer\ntoken. Re-validates and replaces the mutable metadata (name, redirect URIs, and\nrequestable scopes); the client type and its secret lifecycle are fixed at\nregistration and cannot be changed. Returns the updated client metadata.","operationId":"update_client","parameters":[{"name":"client_id","in":"path","description":"Client id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMetadata"}}},"required":true},"responses":{"200":{"description":"Updated client metadata (RFC 7592)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientInfo"}}}},"400":{"description":"Invalid metadata"},"401":{"description":"Invalid registration_access_token"}}},"delete":{"tags":["auth"],"summary":"Delete an OAuth client","description":"RFC 7592 deregistration of a dynamically registered OAuth client identified by\nclient_id in the path, authenticated with the client's registration_access_token as\na Bearer token. Soft-disables the client so it can no longer be used, returning 204\nNo Content on success.","operationId":"delete_client","parameters":[{"name":"client_id","in":"path","description":"Client id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Client deregistered (soft-disabled), RFC 7592"},"401":{"description":"Invalid registration_access_token"}}}},"/v1/oauth/revoke":{"post":{"tags":["auth"],"summary":"Revoke a token","description":"RFC 7009 token revocation: the calling client authenticates (client_id, plus\nclient_secret for confidential clients) and submits a token to revoke, as form or\nJSON. If the token belongs to the authenticating client, it and its entire refresh\nfamily (the paired access and refresh tokens plus all rotations) are revoked. Always\nreturns 200, including for unknown, already-revoked, or not-owned tokens, so it\nnever reveals whether a token exists or who owns it.","operationId":"oauth_revoke","requestBody":{"description":"RFC 7009. Revokes the token and its refresh family. Form or JSON. Always returns 200.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeReq"}}},"required":true},"responses":{"200":{"description":"Revoked (unknown / not-yours / already-revoked all return 200)"},"401":{"description":"Invalid client credentials"},"429":{"description":"Too many requests (per-IP rate limit)"}}}},"/v1/oauth/token":{"post":{"tags":["auth"],"summary":"OAuth token endpoint","description":"Exchanges an authorization code or a refresh token for tokens, accepting either\napplication/x-www-form-urlencoded or application/json. For\ngrant_type=authorization_code it verifies PKCE (S256 code_verifier against the\nstored challenge) and that client_id and redirect_uri match the original\nauthorization; for grant_type=refresh_token it rotates the presented refresh token,\ncarrying the original scopes forward, and detects reuse of an already-rotated token\nby revoking the entire token family. Confidential clients must authenticate with\nclient_secret; public clients rely on PKCE. Returns an opaque short-lived Bearer\naccess_token (expires_in seconds), a new rotating refresh_token, the token_type, and\nthe space-delimited granted scope.","operationId":"oauth_token","requestBody":{"description":"authorization_code or refresh_token grant. Accepts application/x-www-form-urlencoded (the OAuth default) OR application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenReq"}}},"required":true},"responses":{"200":{"description":"Access token + rotating refresh token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResp"}}}},"400":{"description":"invalid_grant / invalid_request"},"401":{"description":"invalid_client"},"429":{"description":"Too many requests (per-IP rate limit)"}}}},"/v1/skills/catalog":{"get":{"tags":["integrations"],"summary":"Get the skills catalog","description":"Returns the full catalog of available skills along with the calling workspace's\ninstall state for each. For every active skill it reports metadata (display name,\ndescription, category, publisher, runtime profile, version, trust tier, and any\nfeatured ranking), whether it is system-locked (always on), its declared\nprerequisites, and its required integrations each annotated with whether that\nintegration is currently met for this tenant. It also reports whether the tenant\nexplicitly enabled or disabled the skill, the effective enabled state (always true\nwhen locked), the install status, and an icon URL when one exists. Requires agents\nread access; scoped to the caller's own workspace.","operationId":"skills_catalog","responses":{"200":{"description":"Skills registry + this tenant's install state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResp"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/skills/catalog/{slug}/content":{"get":{"tags":["integrations"],"summary":"Get skill content","description":"Returns the full content of a skill pack, including its SKILL.md (listed first) and\nany additional files, with the skill's display name, description, runtime profile,\nand trust tier. Inline text files include their UTF-8 contents; large external\nassets are listed but returned with empty content. The editable flag is true only\nfor the caller's own workspace-authored skills and false for platform skills.\nRequires agents read access; scoped to the caller's own workspace.","operationId":"skill_content","parameters":[{"name":"slug","in":"path","description":"Skill slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full skill pack (SKILL.md + files)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillContentResp"}}}},"404":{"description":"No skill with that slug"}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/skills/catalog/{slug}/icon":{"get":{"tags":["integrations"],"summary":"Get a skill icon","description":"Returns the SVG icon for the given skill as image/svg+xml. The response is public,\nimmutable, and long-cached; a 404 is returned when the skill has no icon or is not\nactive.","operationId":"skill_icon","parameters":[{"name":"slug","in":"path","description":"Skill slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The skill's SVG icon (public, immutable, long-cached)"},"404":{"description":"No icon for this skill"}}}},"/v1/skills/installs":{"post":{"tags":["integrations"],"summary":"Install a skill","description":"Installs or re-enables a skill for the caller's workspace, marking it active and\nenabled. Accepts an optional per-tenant config blob. System-locked skills cannot be\ninstalled this way (they are always on) and unknown or inactive slugs return 404.\nIdempotent: re-installing updates the existing entry and preserves the prior config\nwhen omitted. Returns the slug and enabled flag. Admin only; scoped to the caller's\nown workspace.","operationId":"install_skill","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallReq"}}},"required":true},"responses":{"200":{"description":"Skill installed / re-enabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResp"}}}},"400":{"description":"Skill is system-locked (always on)"},"403":{"description":"Admin role required"},"404":{"description":"No active skill with that slug"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/skills/installs/{slug}":{"delete":{"tags":["integrations"],"summary":"Uninstall a skill","description":"Disables a skill for the caller's workspace, opting it out even if it is enabled by\ndefault with no prior explicit install. System-locked skills cannot be disabled\n(they are always on). Returns the slug with enabled set to false. Admin only; scoped\nto the caller's own workspace.","operationId":"uninstall_skill","parameters":[{"name":"slug","in":"path","description":"Skill slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Skill opted out / disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResp"}}}},"400":{"description":"Skill is system-locked (always on)"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/skills/upload":{"post":{"tags":["integrations"],"summary":"Upload a skill","description":"Creates a workspace-owned skill from caller-supplied content. The body may be either\ninline markdown (wrapped automatically as SKILL.md) or a multi-file pack of\nbase64-encoded files that must include a SKILL.md at the root. The display name must\nbe 3 to 60 characters of letters, digits, spaces, hyphens, or underscores and is\nused to derive the slug; an optional runtime profile (claude-sdk, hermes, or\nopenai-agents; defaults to claude-sdk) and an optional capability slot may be set.\nSize limits apply (up to 50 files, 256 KiB total raw content, 64 KiB markdown, and a\n1 MB request limit). Re-uploading with the same derived slug updates the existing\nskill in place. Returns the resulting slug and version with HTTP 201. Admin only;\nscoped to the caller's own workspace.","operationId":"upload_skill","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadReq"}}},"required":true},"responses":{"201":{"description":"Workspace skill created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResp"}}}},"400":{"description":"Invalid pack"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/skills/{slug}":{"delete":{"tags":["integrations"],"summary":"Delete a skill","description":"Permanently deletes a workspace-owned skill and its install records. Only the\ncaller's own tenant skills (those whose slug carries the tenant prefix) and only\nworkspace-authored (T3) skills may be deleted; attempting to delete a platform skill\nor another tenant's skill is forbidden. Returns the slug with deleted set to true.\nAdmin only; scoped to the caller's own workspace.","operationId":"delete_skill","parameters":[{"name":"slug","in":"path","description":"Workspace skill slug (t:<tenant>:<name>)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Workspace skill deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResp"}}}},"403":{"description":"Admin role required, or not a workspace-owned skill"},"404":{"description":"No skill with that slug"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}}},"components":{"schemas":{"AboutMeResp":{"type":"object","required":["facts","fact_types_available"],"properties":{"fact_types_available":{"type":"array","items":{"type":"string"},"description":"Distinct fact_types present in the caller's full set (not the\nfiltered page). Used by the UI to render filter chips -\ncaller doesn't have to scan the whole set to know what's\nin there."},"facts":{"type":"array","items":{"$ref":"#/components/schemas/FactEntry"}},"next_cursor":{"type":["string","null"],"description":"Cursor to pass on the next page. `None` once the caller has\nreached the tail of the result set."},"persona_generated_at":{"type":["string","null"],"format":"date-time","description":"When `persona_paragraph` was last refreshed. UI shows \"updated\nX minutes ago\" so the user can request a re-distill if the\nparagraph is stale relative to recent activity."},"persona_paragraph":{"type":["string","null"],"description":"Per docs/agent-runtime.md §System prompt envelope, layer 4\n(the user persona). Rendered once per (tenant, user, ~day) from\nthe structured `user_profile.profile` JSONB by\n`hq-reflection-worker`. Surfaced FIRST in this response so\nthe user reads + corrects the one-paragraph view in a single\nscreen before diving into the per-fact list.\n`None` when the persona hasn't been distilled yet."}}},"AcceptReq":{"type":"object","properties":{"bind_user_id":{"type":["string","null"],"format":"uuid","description":"Bind the quarantined sender to this HQ user so their mail routes from\nnow on. Required to clear an `unknown_sender` quarantine."}}},"ActionOk":{"type":"object","required":["ok"],"properties":{"handling_state":{"type":["string","null"]},"note":{"type":["string","null"]},"ok":{"type":"boolean"}}},"ActivityListResp":{"type":"object","description":"Activity list response: one page of rows + the total count over the whole\n(scoped + filtered) set, so the UI can render \"page X of Y\".","required":["rows","total"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/ActivityRow"}},"total":{"type":"integer","format":"int64"}}},"ActivityRow":{"type":"object","description":"One row of the Activity feed: an inbound or outbound `email_message`,\ndecorated with its endpoint + plane and a deep-link to the conversation\nthread (so opening it reuses the shared conversation renderer).","required":["email_message_id","direction","handling_state","from_addr","to_addrs","occurred_at"],"properties":{"agent_id":{"type":["string","null"],"format":"uuid"},"conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation thread this mail belongs to (channel_kind='email',\nchannel_thread=thread_key). `None` only for a parked item whose\nconversation was never materialised (unrouted / filed)."},"direction":{"type":"string"},"email_message_id":{"type":"string","format":"uuid"},"endpoint_id":{"type":["string","null"],"format":"uuid"},"endpoint_localpart":{"type":["string","null"]},"endpoint_plane":{"type":["string","null"]},"from_addr":{"type":"string"},"handling_state":{"type":"string"},"llm_category":{"type":["string","null"]},"llm_summary":{"type":["string","null"]},"occurred_at":{"type":"string","format":"date-time"},"subject":{"type":["string","null"]},"to_addrs":{"type":"array","items":{"type":"string"}}}},"AddUserReq":{"type":"object","required":["email"],"properties":{"admin_access":{"type":["string","null"],"description":"`default` (normal member) or `granted` (tenant-admin from the start)."},"display_name":{"type":["string","null"]},"email":{"type":"string","description":"Email of the person to add. This is a lookup + dedup attribute only -\nthe identity key is a stable UUID (`external_user_id = workspace_users.id`),\nso the same human survives an email change. One row per (direct source,\nlowercased email); re-inviting updates that row, keeping its UUID."}}},"AgentLite":{"type":"object","required":["display_name"],"properties":{"avatar_url":{"type":["string","null"]},"display_name":{"type":"string"},"id":{"type":["string","null"],"format":"uuid","description":"`None` for an AGENTLESS thread (fast/quick lane, email Quick-reply):\nno agent owns it. `display_name` is then a synthesized label (\"Q\" /\n\"Quick reply\"), not a stored agent."},"slug":{"type":["string","null"]}}},"AgentRow":{"type":"object","required":["id","slug","display_name","instructions_is_default","cautions","languages","aliases","runtime_profile","delivery_mode","is_default","status","created_at"],"properties":{"aliases":{"type":"array","items":{"type":"string"}},"avatar_url":{"type":["string","null"],"description":"Absolute URL of the avatar shown next to this agent's\nmessages. NULL = use the Slack app's default icon for now\n(fine for legacy single-agent tenants)."},"cautions":{"type":"array","items":{"type":"string"},"description":"Free-form short phrases the agent should avoid talking about.\nComposed as \"Be careful around: X, Y, Z\" in the system prompt.\nEmpty = no caution block."},"created_at":{"type":"string","format":"date-time"},"delivery_mode":{"type":"string"},"description":{"type":["string","null"]},"display_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"instructions":{"type":["string","null"],"description":"Admin-editable system-prompt for this agent. NULL = use the\nplatform baseline only (the legacy generalist behaviour).\nPlumbed through to `router_client::compose_system_prompt`\nwhich sandwiches it between the Slack-formatting baseline\nand the identity-pin footer."},"instructions_is_default":{"type":"boolean","description":"True iff `instructions` is still the platform-seeded value\n(admin hasn't customised or tailored yet). The admin UI uses\nthis to surface a \"your agent is still on the default - click\nTailor\" banner. Any PATCH that writes instructions flips\nthis false; the seed migrations set it true on creation."},"is_default":{"type":"boolean"},"languages":{"type":"array","items":{"type":"string"},"description":"BCP-47 language codes. The first is the default; subsequent\nentries are explicit fallbacks. Empty = no language pin\n(LLM follows the user's language)."},"model":{"type":["string","null"],"description":"Selected model slug (e.g. `claude-opus-4-8`). NULL = the runtime's\nplatform default. Must belong to `runtime_profile` (billing C)."},"runtime_profile":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"tone":{"type":["string","null"],"description":"One of `formal | friendly | direct | casual` (or NULL = unset\n→ no tone pin in the system prompt). Orthogonal to role;\nadmin choice, not inferrable from \"you're a CFO\"."},"verbosity":{"type":["string","null"],"description":"One of `concise | balanced | detailed` (or NULL = unset).\nDrives a canonical \"Default to brief / balanced / detailed\nresponses\" sentence in the system prompt."}}},"AppLite":{"type":"object","required":["id","slug","state","visibility","run_mode","declared_port","url","created_at","last_activity_at"],"properties":{"backing_conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation that authored the app - lets the UI deep-link\nto the Studio thread. `None` if the backing conv was deleted."},"created_at":{"type":"integer","format":"int64","description":"Unix seconds - the UI humanises (\"2 days ago\")."},"declared_port":{"type":"integer","format":"int32"},"id":{"type":"string","format":"uuid"},"last_activity_at":{"type":"integer","format":"int64"},"run_mode":{"type":"string"},"slug":{"type":"string"},"state":{"type":"string"},"url":{"type":"string","description":"Visitor-facing URL (bare for public, signed plain-host for\nprivate). Empty-ish only when no HMAC secret is loaded."},"visibility":{"type":"string","description":"\"public\" | \"private\" - derived from `auth_mode`. The surfaces\ntable has no `visibility` column; public == `auth_mode='public'`."}}},"AppSummary":{"type":"object","required":["client_id","name","client_type","redirect_uris","scopes","created_at"],"properties":{"client_id":{"type":"string"},"client_type":{"type":"string","description":"`public` (PKCE, no secret) or `confidential` (has a secret)."},"created_at":{"type":"string"},"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}}}},"AppsResp":{"type":"object","required":["apps","max_persistent","used"],"properties":{"apps":{"type":"array","items":{"$ref":"#/components/schemas/AppLite"}},"max_persistent":{"type":"integer","format":"int32","description":"The apps quota - plan-driven (`plan.max_apps`) with a fallback to the\nlegacy `tenants.max_persistent_surfaces` - and how many non-destroyed\napps count against it. Drives the \"N of M slots used\" line."},"used":{"type":"integer","minimum":0}}},"ArtifactLite":{"type":"object","required":["id","name","content_type","size_bytes","sensitive","created_at","download_url"],"properties":{"content_type":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"download_url":{"type":"string"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"sensitive":{"type":"boolean"},"size_bytes":{"type":"integer","format":"int64"}}},"ArtifactRow":{"type":"object","required":["id","name","content_type","size_bytes","sensitive","created_at","download_url"],"properties":{"caption":{"type":["string","null"]},"content_type":{"type":"string"},"conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation that produced it; `None` once detached (its\nconversation was deleted with \"keep\")."},"conversation_title":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"download_url":{"type":"string"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"sensitive":{"type":"boolean"},"size_bytes":{"type":"integer","format":"int64"}}},"ArtifactsResp":{"type":"object","required":["artifacts"],"properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/ArtifactRow"}}}},"AskReq":{"type":"object","required":["text"],"properties":{"conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation the user is viewing. The browser ask runs in THIS\nthread (per-thread browser session), so a new thread = a fresh agent\nsession - no inherited stuck-session state. Omit only for legacy\nclients, which fall back to a deprecated stable per-user thread."},"text":{"type":"string","description":"The user's request (e.g. \"what is this page\" / \"fill in my name\")."}}},"AskResp":{"type":"object","required":["conversation_id","reply"],"properties":{"conversation_id":{"type":"string","description":"The conversation the ask ran in. The client stores this and sends it\nback as `conversation_id` on the next ask - exactly like the normal /\nfast turn endpoints. (First ask with no id creates the thread.)"},"reply":{"type":"string","description":"The agent's reply text."}}},"AssignReq":{"type":"object","required":["agent_id"],"properties":{"agent_id":{"type":"string","format":"uuid"}}},"AttachmentInput":{"type":"object","description":"One file attached to an inbound user message. Two delivery\nmodes, mutually exclusive:\n\n  * **Inline bytes** (`data_b64`) - legacy path. The caller already\n    has the bytes in hand (e.g. slack-transport just downloaded\n    them from Slack) and ships them directly in the request body.\n    Bounded by the JSON body limit.\n\n  * **Document reference** (`document_id`) - SOTA path. The file\n    is already in Trove behind a stable `documents` row (Studio\n    drag-drop, an earlier `documents.save`, an OAuth-shared file).\n    The turn driver resolves the id, loads bytes from Trove, and\n    builds the same downstream `Attachment` shape -- the agent\n    doesn't see the distinction.\n\n`filename` is optional only on the `document_id` path (the row\nalready carries one); inline callers must supply it. `content_type`\nfollows the same rule.","properties":{"content_type":{"type":["string","null"]},"data_b64":{"type":["string","null"],"description":"Inline base64 bytes. Provide this OR `document_id`, not both."},"document_id":{"type":["string","null"],"format":"uuid","description":"Reference to an existing `documents` row in the caller's tenant.\nThe turn driver loads bytes from Trove server-side."},"filename":{"type":["string","null"]}}},"AuditRow":{"type":"object","required":["id","seq","occurred_at","actor_kind","kind","outcome","entry_hash"],"properties":{"actor_id":{"type":["string","null"],"format":"uuid"},"actor_kind":{"type":"string"},"agent_id":{"type":["string","null"],"format":"uuid"},"agent_version":{"type":["string","null"]},"args_redacted":{},"capability_scope":{"type":["string","null"]},"conversation_id":{"type":["string","null"],"format":"uuid"},"entry_hash":{"type":"string"},"id":{"type":"string","format":"uuid"},"intent":{"type":["string","null"]},"kind":{"type":"string"},"latency_ms":{"type":["integer","null"],"format":"int32"},"occurred_at":{"type":"string","format":"date-time"},"on_behalf_of":{"type":["string","null"],"format":"uuid"},"outcome":{"type":"string"},"prev_hash":{"type":["string","null"]},"result_hash":{"type":["string","null"]},"seq":{"type":"integer","format":"int64"},"source":{"type":["string","null"]},"target":{"type":["string","null"]}}},"Authorization":{"type":"object","required":["client_id","name","client_type","active","scopes","authorized_at","updated_at"],"properties":{"active":{"type":"boolean","description":"Whether the app is still registered and active. A disabled app keeps the\nconsent row until revoked, but its tokens no longer refresh."},"authorized_at":{"type":"string","description":"When the app was first authorized."},"client_id":{"type":"string"},"client_type":{"type":"string","description":"`public` (PKCE) or `confidential` (has a secret)."},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"},"description":"The capability scopes this user has granted the app (the running UNION)."},"updated_at":{"type":"string","description":"When the grant last grew (a re-auth requesting new scopes)."}}},"AvatarUploadReq":{"type":"object","required":["filename","content_type","body_b64"],"properties":{"body_b64":{"type":"string"},"content_type":{"type":"string"},"filename":{"type":"string"}}},"Badge":{"type":"object","required":["count","awaiting_review","unrouted","quarantined"],"properties":{"awaiting_review":{"type":"integer","format":"int64"},"count":{"type":"integer","format":"int64","description":"Sum of the three queues the caller can see (the nav-badge number)."},"quarantined":{"type":"integer","format":"int64"},"unrouted":{"type":"integer","format":"int64"}}},"CatalogResp":{"type":"object","required":["skills"],"properties":{"skills":{"type":"array","items":{"$ref":"#/components/schemas/CatalogSkill"}}}},"CatalogServer":{"type":"object","required":["slug","display_name","publisher","transport","trust_tier","version","default_enabled","credential_model","effective_enabled","allowed_scopes","featured"],"properties":{"allowed_scopes":{"type":"array","items":{"type":"string"},"description":"Which install scopes the operator permits for this MCP.\nSubset of `{team, user}`. The UI shows the Team/Private\npicker only when both are present; single-element arrays\nskip the picker and default silently."},"category":{"type":["string","null"]},"credential_model":{"type":"string","description":"`none` / `platform` / `tenant_api_key` / `tenant_oauth`. Drives\nthe install UX - `none` + `platform` are one-click toggles;\n`tenant_oauth` routes through `/v1/oauth/start/{slug}`;\n`tenant_api_key` prompts for a vault-bound credential."},"default_enabled":{"type":"boolean"},"description":{"type":["string","null"]},"display_name":{"type":"string"},"effective_enabled":{"type":"boolean","description":"Effective state for the calling tenant - `default_enabled ⊕\ntenant_enabled`."},"featured":{"type":"boolean","description":"Promoted into the Featured strip; `featured_rank` orders it."},"featured_rank":{"type":["integer","null"],"format":"int32"},"icon_url":{"type":["string","null"],"description":"Origin-served icon URL (`/v1/mcp/catalog/{slug}/icon?v=<md5>`) when\nthis server has an `icon_svg`; `None` falls back to a lettermark\ntile in the UI. The `?v=` token busts the immutable cache when the\nicon changes."},"install_status":{"type":["string","null"],"description":"Tenant install status (`active` / `paused` / `failed` /\n`reauth_required`) when an explicit row exists. Useful for\nsurfacing \"Reconnect\" prompts in the UI."},"publisher":{"type":"string"},"slug":{"type":"string"},"tenant_enabled":{"type":["boolean","null"],"description":"What this tenant has done explicitly. `None` = no row, inherits\nfrom `default_enabled`. `Some(true/false)` = explicit\ninstall/disable."},"transport":{"type":"string"},"trust_tier":{"type":"string"},"version":{"type":"string"}}},"CatalogSkill":{"type":"object","required":["slug","display_name","publisher","profile","version","default_enabled","trust_tier","locked","requires","required_integrations","effective_enabled","featured"],"properties":{"category":{"type":["string","null"]},"default_enabled":{"type":"boolean"},"description":{"type":["string","null"]},"display_name":{"type":"string"},"effective_enabled":{"type":"boolean"},"featured":{"type":"boolean","description":"Promoted into the Featured strip; `featured_rank` orders it."},"featured_rank":{"type":["integer","null"],"format":"int32"},"icon_url":{"type":["string","null"],"description":"Origin-served icon URL (`/v1/skills/catalog/{slug}/icon?v=<md5>`)\nwhen this skill has an `icon_svg`; `None` falls back to a\nlettermark tile in the UI."},"install_status":{"type":["string","null"]},"locked":{"type":"boolean"},"profile":{"type":"string"},"publisher":{"type":"string"},"required_integrations":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationDep"}},"requires":{"type":"array","items":{"type":"string"}},"slug":{"type":"string"},"tenant_enabled":{"type":["boolean","null"]},"trust_tier":{"type":"string"},"version":{"type":"string"}}},"ChannelJson":{"type":"object","required":["id","name","is_private"],"properties":{"id":{"type":"string"},"is_private":{"type":"boolean"},"name":{"type":"string"}}},"ChannelsResp":{"type":"object","required":["connected","channels"],"properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelJson"}},"connected":{"type":"boolean","description":"Whether this workspace has a live (non-revoked) Slack install.\n`false` means the picker should prompt to connect Slack rather\nthan show an empty list."}}},"CheckoutReq":{"type":"object","required":["plan_slug"],"properties":{"plan_slug":{"type":"string"}}},"ClientInfo":{"type":"object","required":["client_id","client_id_issued_at","registration_client_uri","redirect_uris","client_name","token_endpoint_auth_method","grant_types","response_types","scope"],"properties":{"client_id":{"type":"string"},"client_id_issued_at":{"type":"integer","format":"int64"},"client_name":{"type":"string"},"client_secret":{"type":["string","null"]},"client_secret_expires_at":{"type":["integer","null"],"format":"int64"},"grant_types":{"type":"array","items":{"type":"string"}},"redirect_uris":{"type":"array","items":{"type":"string"}},"registration_access_token":{"type":["string","null"]},"registration_client_uri":{"type":"string"},"response_types":{"type":"array","items":{"type":"string"}},"scope":{"type":"string"},"token_endpoint_auth_method":{"type":"string"}}},"ClientMetadata":{"type":"object","properties":{"client_name":{"type":["string","null"]},"grant_types":{"type":["array","null"],"items":{"type":"string"}},"redirect_uris":{"type":"array","items":{"type":"string"}},"response_types":{"type":["array","null"],"items":{"type":"string"}},"scope":{"type":["string","null"],"description":"Space-delimited capability scopes the client may request."},"token_endpoint_auth_method":{"type":["string","null"]}}},"ConfigPatch":{"type":"object","description":"The editable behavior fields. All optional on PATCH (a full-form replace);\non create, missing values fall back to the DB column defaults.","properties":{"allow_escalation":{"type":["boolean","null"]},"allow_patterns":{"type":["array","null"],"items":{"type":"string"},"description":"Replaces the allowlist set wholesale when present."},"category_policy":{},"default_reply_lang":{"type":["string","null"]},"deliver_to":{},"enabled":{"type":["boolean","null"]},"instructions":{"type":["string","null"]},"max_turns_per_day":{"type":["integer","null"],"format":"int32"},"max_turns_per_sender_day":{"type":["integer","null"],"format":"int32"},"plane":{"type":["string","null"]},"reply_policy":{"type":["string","null"]},"sender_policy":{"type":["string","null"]},"spam_score_max":{"type":["number","null"],"format":"float"}}},"ConnectStatus":{"type":"object","required":["slack","teams","extension","recommendations","detected"],"properties":{"detected":{"type":"array","items":{"$ref":"#/components/schemas/DetectedItem"},"description":"Everything the profiler detected about the org's site/DNS - shown so the\ntenant sees the result even when nothing maps to a connectable\nrecommendation (e.g. HubSpot/Mailchimp, which we have no connector for yet)."},"extension":{"$ref":"#/components/schemas/ExtensionState"},"recommendations":{"type":"array","items":{"$ref":"#/components/schemas/RecItem"}},"slack":{"$ref":"#/components/schemas/SlackState"},"teams":{"$ref":"#/components/schemas/TeamsState"}}},"ConvAgent":{"type":"object","required":["display_name"],"properties":{"avatar_url":{"type":["string","null"]},"display_name":{"type":"string"},"id":{"type":["string","null"],"format":"uuid","description":"`None` for an agentless thread (fast/quick lane, email Quick-reply)."},"slug":{"type":["string","null"]}}},"ConvApp":{"type":"object","required":["id","slug","state"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"state":{"type":"string"}}},"ConvArtifactsResp":{"type":"object","required":["artifacts"],"properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/ArtifactLite"}}}},"ConvListItem":{"type":"object","required":["id","channel_kind","mode","last_activity_at","agent","artifact_count"],"properties":{"agent":{"$ref":"#/components/schemas/ConvAgent"},"app":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ConvApp"}]},"artifact_count":{"type":"integer","format":"int64","description":"How many delivered artifacts this conversation has - the delete\nmodal uses it to show + gate the \"also delete delivered files\"\ncheckbox (hidden when 0)."},"channel_kind":{"type":"string"},"email_from":{"type":["string","null"],"description":"Latest email message on this thread (from / to / subject). Only set for\nemail-channel conversations; NULL elsewhere. Backs the Email > Threads\nlist so it shows the same From/To/Subject as every other email row."},"email_subject":{"type":["string","null"]},"email_to":{"type":["array","null"],"items":{"type":"string"}},"id":{"type":"string","format":"uuid"},"last_activity_at":{"type":"string","format":"date-time"},"last_text":{"type":["string","null"]},"mode":{"type":"string","description":"`standard` (sandbox agent thread) or `fast` (no-VM quick ask). Lets\nthe home \"Continue\" list (which requests `mode=all`) brand fast rows\nas quick asks instead of the bound agent's persona."},"title":{"type":["string","null"],"description":"Worker-derived short label (4-7 words). NULL until the\ntitle.derive worker has processed this conv; the SPA falls\nback to `last_text` in that case, rendered muted so the user\nreads it as a placeholder."}}},"ConvListResp":{"type":"object","required":["conversations","viewing_as","total","page","page_size"],"properties":{"conversations":{"type":"array","items":{"$ref":"#/components/schemas/ConvListItem"}},"page":{"type":"integer","format":"int64","description":"Echo of the resolved page + page size (after clamping) so\nthe SPA renders the same window it asked for."},"page_size":{"type":"integer","format":"int64"},"total":{"type":"integer","format":"int64","description":"Total rows matching the (search + ACL) filter, before\nLIMIT/OFFSET - drives the SPA's pagination control."},"viewing_as":{"type":"string","description":"`\"participant\"` (default) or `\"admin\"` - tells the SPA\nwhether to render the \"Admin view\" banner. Admin view is\naudited via `conversation_access_audit`; the SPA showing\nthe banner is the visible half of that observability."}}},"ConvMetaResp":{"type":"object","required":["conversation_id","agent","viewing_as","mode","channel_kind","surface","read_only","participants"],"properties":{"agent":{"$ref":"#/components/schemas/AgentLite"},"channel_kind":{"type":"string","description":"Origin channel: `web` / `slack` / `teams` / `schedule`. Studio\nrenders `schedule` threads read-only (a record of a scheduled run,\nnot a live chat) and offers \"start a fresh conversation\" instead."},"conversation_id":{"type":"string","format":"uuid"},"mode":{"type":"string","description":"`standard` (sandbox agent thread) or `fast` (no-VM \"ask\" thread).\nStudio shows a \"Convert to agent\" affordance on fast threads."},"participants":{"description":"Directory of every user the thread references (message authors +\n`<@U…>` mentions), keyed by UID -> { name, avatar_url }. The SPA renders\n`<@U…>` mention chips from this. `{}` for single-party (web/DM) convs."},"read_only":{"type":"boolean","description":"True when this conversation is read-only from web/extension: a\nmulti-party Slack/Teams channel thread (owned by Slack/Teams). The SPA\ndisables the composer + shows a \"happening in the channel\" hint. The\nAPI enforces this independently (prepare_turn refuses such turns)."},"surface":{"type":"string","description":"Conversation surface: `channel` (multi-party Slack/Teams thread) /\n`dm` / `web` / `extension`. Drives `read_only`."},"title":{"type":["string","null"],"description":"Display label for the conversation. User-set (Studio rename) or\nauto-derived (title.derive worker); NULL until one exists, in\nwhich case the SPA falls back to a message preview / \"Untitled\"."},"viewing_as":{"type":"string","description":"`\"participant\"` (default) or `\"admin\"` when the caller\nelevated via `?view=admin` AND is `workspace_users.is_admin`.\nThe SPA renders an \"Admin view\" banner when this is `admin`\nso the user knows they're looking at someone else's conv."}}},"ConvSurfacesResp":{"type":"object","required":["surfaces"],"properties":{"surfaces":{"type":"array","items":{"$ref":"#/components/schemas/SurfaceLite"}}}},"CorrectInput":{"type":"object","required":["new_fact"],"properties":{"new_fact":{"type":"string"}}},"CorrectResp":{"type":"object","required":["old_fact_id","new_fact_id"],"properties":{"new_fact_id":{"type":"string","format":"uuid"},"old_fact_id":{"type":"string","format":"uuid"}}},"Counts":{"type":"object","required":["schedules_owned"],"properties":{"schedules_owned":{"type":"integer","format":"int64"}}},"CreateConvReq":{"type":"object","required":["agent_id"],"properties":{"agent_id":{"type":"string","format":"uuid","description":"The agent the user picked in the modal. Tenant-scoped: we\nrefuse to create against an agent_id that doesn't live in\nthe caller's workspace."}}},"CreateConvResp":{"type":"object","required":["id","channel_kind","channel_thread","adapter_conv_id","agent_id"],"properties":{"adapter_conv_id":{"type":"string"},"agent_id":{"type":"string","format":"uuid"},"channel_kind":{"type":"string"},"channel_thread":{"type":"string"},"id":{"type":"string","format":"uuid"}}},"CreateInput":{"type":"object","required":["name","trigger","prompt","deliver_to"],"properties":{"agent_id":{"type":["string","null"],"format":"uuid"},"deliver_to":{"type":"object"},"delivery_locale":{"type":["string","null"],"description":"Optional BCP-47 locale override for this schedule's deliveries.\nFalls back to the owner's `workspace_users.locale` when absent."},"description":{"type":["string","null"]},"name":{"type":"string"},"prompt":{"type":"string"},"reset_each_fire":{"type":["boolean","null"]},"trigger":{"type":"object"}}},"CreateReq":{"allOf":[{"$ref":"#/components/schemas/ConfigPatch"},{"type":"object","required":["localpart","kind"],"properties":{"agent_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string"},"localpart":{"type":"string"}}}]},"CreateResp":{"type":"object","required":["schedule"],"properties":{"schedule":{"$ref":"#/components/schemas/ScheduleJson"}}},"CreateTokenReq":{"type":"object","required":["name"],"properties":{"expires_in_days":{"type":["integer","null"],"format":"int64","description":"Optional expiry; omitted = non-expiring manual PAT."},"name":{"type":"string"},"scopes":{"type":["array","null"],"items":{"type":"string"},"description":"Requested capability scopes (resource:action, e.g. `documents:read`).\nOmitted = `[\"user\"]` (full non-admin access, back-compat). Each must\nbe a known capability scope or a legacy tier; the effective set is\nstill clamped to the caller's role at request time."}}},"CreateTokenResp":{"type":"object","required":["id","token","name","scopes"],"properties":{"expires_at":{"type":["string","null"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"token":{"type":"string","description":"The plaintext token - shown exactly once, never retrievable again."}}},"CreatedApp":{"type":"object","required":["client_id","registration_access_token","name","client_type","redirect_uris","scopes"],"properties":{"client_id":{"type":"string"},"client_secret":{"type":["string","null"],"description":"Present only for confidential clients; shown ONCE, never retrievable."},"client_type":{"type":"string"},"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"registration_access_token":{"type":"string","description":"The RFC 7592 management bearer; shown ONCE."},"scopes":{"type":"array","items":{"type":"string"}}}},"DeleteArtifactResp":{"type":"object","required":["id","deleted"],"properties":{"deleted":{"type":"boolean"},"id":{"type":"string","format":"uuid"}}},"DeleteConvResp":{"type":"object","required":["conversation_id","apps_destroyed","artifacts_deleted","artifacts_kept"],"properties":{"apps_destroyed":{"type":"integer","minimum":0},"artifacts_deleted":{"type":"integer","description":"Delivered artifacts whose Trove blobs + rows were deleted (when\nthe caller asked to delete them).","minimum":0},"artifacts_kept":{"type":"integer","description":"Delivered artifacts detached (`conversation_id` -> NULL) and kept\nin Trove - the default. They survive the conversation, still\ndownloadable by their existing `/v1/api/artifacts/{id}/download`\nlink, just no longer tied to a (now-deleted) conversation.","minimum":0},"conversation_id":{"type":"string","format":"uuid"}}},"DeleteResp":{"type":"object","required":["slug","deleted"],"properties":{"deleted":{"type":"boolean"},"slug":{"type":"string"}}},"DeletedResp":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"boolean"}}},"DetectedItem":{"type":"object","required":["source","signal_type","value","confidence"],"properties":{"confidence":{"type":"number","format":"float"},"signal_type":{"type":"string"},"source":{"type":"string"},"value":{"type":"string"}}},"DetectedSignal":{"type":"object","required":["signal_type","value"],"properties":{"signal_type":{"type":"string"},"value":{"type":"string"}}},"DocumentRow":{"type":"object","required":["id","owner_user_id","scope","filename","content_type","size_bytes","sha256","tags","created_at","updated_at","download_url","is_owner","classification_status","auto_categorized","reference_numbers","profile_version"],"properties":{"analysis_strategy":{"type":["string","null"],"description":"Which classifier pipeline produced this row\n(TextOnly / VisionDirect / Hybrid / Image / PassThroughText).\nNULL on rows from before the smart-strategy slice."},"auto_categorized":{"type":"boolean","description":"True once the classifier worker has written back the\nauto-derived category/summary/tags. Distinct from `category\nIS NOT NULL` because users can set categories manually too."},"caption":{"type":["string","null"]},"category":{"type":["string","null"]},"channel_id":{"type":["string","null"],"format":"uuid"},"channel_name":{"type":["string","null"]},"classification_confidence":{"type":["number","null"],"format":"float","description":"Self-reported model certainty (0..1) about the classification.\nUI flags rows below 0.6 as \"needs review\"."},"classification_status":{"type":"string","description":"`pending` / `queued` / `processing` / `completed` / `failed` /\n`skipped`. UI shows a pill until this hits `completed`."},"content_type":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"document_date":{"type":["string","null"],"format":"date-time","description":"What the doc refers to (issue / publication / statement date),\nnot when we ingested it. NULL when not determinable."},"download_url":{"type":"string"},"entities":{"description":"LLM-extracted entities ({person_names, company_names, identifiers, amounts, dates}).\nJSONB blob; UI renders selectively."},"filename":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_owner":{"type":"boolean","description":"True iff the caller is the document's owner. Drives \"Delete /\nEdit\" affordances in the UI."},"language":{"type":["string","null"],"description":"ISO 639-1 code (`en`, `sv`, ...). NULL when not determinable."},"owner_display":{"type":["string","null"]},"owner_user_id":{"type":"string","format":"uuid"},"profile_version":{"type":"string","description":"Schema version - `hq-document-classify-v1` for legacy rows,\n`hq-document-classify-v2` once the classifier writes back\nthe entities + dates surface."},"reference_numbers":{"type":"array","items":{"type":"string"},"description":"Document-specific IDs (invoice/case/order numbers). Free-form."},"scope":{"type":"string"},"sha256":{"type":"string"},"size_bytes":{"type":"integer","format":"int64"},"source_ref":{"type":["string","null"]},"source_url":{"type":["string","null"]},"summary":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"updated_at":{"type":"string","format":"date-time"}}},"DomainReq":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string"}}},"EndpointJson":{"type":"object","required":["id","localpart","kind","plane","allow_escalation","sender_policy","reply_policy","enabled","is_system","allow_patterns","message_count","needs_attention","received","replied","awaiting_review","unrouted","escalated","reply_languages","created_at","updated_at"],"properties":{"agent_id":{"type":["string","null"],"format":"uuid"},"allow_escalation":{"type":"boolean"},"allow_patterns":{"type":"array","items":{"type":"string"}},"awaiting_review":{"type":"integer","format":"int64"},"category_policy":{},"created_at":{"type":"string","format":"date-time"},"default_reply_lang":{"type":["string","null"]},"deliver_to":{},"enabled":{"type":"boolean"},"escalated":{"type":"integer","format":"int64"},"id":{"type":"string","format":"uuid"},"instructions":{"type":["string","null"]},"is_system":{"type":"boolean","description":"True for the workspace outbound mailbox (HQ-managed): not deletable, not\na normal custom inbox - the UI renders it as the workspace mailbox."},"kind":{"type":"string"},"last_occurred_at":{"type":["string","null"],"format":"date-time"},"localpart":{"type":"string"},"max_turns_per_day":{"type":["integer","null"],"format":"int32"},"max_turns_per_sender_day":{"type":["integer","null"],"format":"int32"},"message_count":{"type":"integer","format":"int64","description":"Total mail (in + out) attributed to this endpoint."},"needs_attention":{"type":"integer","format":"int64","description":"Items needing a human (handling_state <> 'handled')."},"plane":{"type":"string"},"received":{"type":"integer","format":"int64","description":"Disposition counters (E7.2): inbound received, outbound replies sent,\nper-state queues, and threads escalated light -> agent."},"replied":{"type":"integer","format":"int64"},"reply_languages":{"description":"Reply-language distribution: `{ \"English\": 12, \"Swedish\": 3 }`."},"reply_policy":{"type":"string"},"sender_policy":{"type":"string"},"spam_score_max":{"type":["number","null"],"format":"float"},"unrouted":{"type":"integer","format":"int64"},"updated_at":{"type":"string","format":"date-time"}}},"EndpointListJson":{"type":"object","required":["outbound_mailbox_localpart","endpoints"],"properties":{"domain":{"type":["string","null"]},"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/EndpointJson"}},"outbound_mailbox_localpart":{"type":"string","description":"The localpart of the workspace outbound mailbox (default 'team'); the\nfull address is `<localpart>-<tenant_email_slug>@<domain>`."},"tenant_email_slug":{"type":["string","null"],"description":"The tenant's canonical email slug + domain so the UI can preview the\nfull `<localpart>-<slug>@<domain>` address without another call."}}},"Enrichment":{"type":"object","properties":{"enriched_at":{"type":["string","null"],"format":"date-time"},"industry":{"type":["string","null"]},"segment":{"type":["string","null"]},"size_hint":{"type":["string","null"]}}},"EraseReq":{"type":"object","properties":{"reason":{"type":["string","null"],"description":"Optional free-text justification. Captured in the redaction\nreason so the audit trail surfaces *why*. e.g.\n\"GDPR Article 17 deletion request, ticket #1234\"."}}},"ExtensionState":{"type":"object","properties":{"download_url":{"type":["string","null"],"description":"Self-hosted packaged build (our origin), when available."},"web_store_url":{"type":["string","null"],"description":"Chrome Web Store listing, when published."}}},"FactEntry":{"type":"object","required":["id","fact","fact_type","confidence","agent_slug","asserted_at","source_conv_id"],"properties":{"agent_slug":{"type":"string"},"asserted_at":{"type":"string","format":"date-time"},"confidence":{"type":"number","format":"float"},"fact":{"type":"string"},"fact_type":{"type":"string"},"id":{"type":"string","format":"uuid"},"source_conv_id":{"type":"string","format":"uuid"}}},"ForgetResp":{"type":"object","required":["fact_id","status"],"properties":{"fact_id":{"type":"string","format":"uuid"},"status":{"type":"string"}}},"FreezableAgent":{"type":"object","required":["agent_slug","frozen"],"properties":{"agent_slug":{"type":"string"},"frozen":{"type":"boolean"},"frozen_at":{"type":["string","null"],"format":"date-time"},"reason":{"type":["string","null"]}}},"FreezableAgentsResp":{"type":"object","required":["agents"],"properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/FreezableAgent"}}}},"FreezeBody":{"type":"object","properties":{"reason":{"type":["string","null"]}}},"FreezeListEntry":{"type":"object","required":["agent_slug","created_at"],"properties":{"agent_slug":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"reason":{"type":["string","null"]}}},"FreezeListResp":{"type":"object","required":["freezes"],"properties":{"freezes":{"type":"array","items":{"$ref":"#/components/schemas/FreezeListEntry"}}}},"FreezeResp":{"type":"object","required":["agent_slug","frozen"],"properties":{"agent_slug":{"type":"string"},"frozen":{"type":"boolean"}}},"Identity":{"type":"object","required":["user_id","external_user_id","tenant_slug","source_kind","is_admin","is_bot","profile_consent","time_format","time_zone","discovered_at"],"properties":{"avatar_url":{"type":["string","null"],"description":"Profile avatar. Always an internal `/v1/api/artifacts/{uuid}/download`\nURL (the schema CHECK constraint refuses anything else) or\nnull if no avatar is on file yet. The SPA can render with\nthe same cookie auth that loaded the page; the browser never\nhits a third-party host."},"discovered_at":{"type":"string","format":"date-time"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]},"external_user_id":{"type":"string","description":"External id from the connected workspace (Slack `U…`, Teams `aad-…`)."},"is_admin":{"type":"boolean"},"is_bot":{"type":"boolean"},"last_seen_at":{"type":["string","null"],"format":"date-time"},"locale":{"type":["string","null"]},"profile_consent":{"type":"string","description":"`pending` / `granted` / `denied` - tenant-side consent for\nper-user persona distillation."},"real_name":{"type":["string","null"]},"source_kind":{"type":"string","description":"Source connector kind - `slack` / `teams` / `manual`."},"source_workspace":{"type":["string","null"],"description":"Display name of the connected workspace (e.g. \"Truespar\")."},"tenant_slug":{"type":"string","description":"Tenant slug the caller is signed in to - the same value\ncarried on `X-Hq-Workspace`. Exposed so the SPA can hold\nonto it without parking it in sessionStorage (which doesn't\nsurvive a direct-navigation download or share link)."},"time_format":{"type":"string","description":"Web-UI clock preference (migration 0169): `auto` / `12h` / `24h`.\n`auto` lets the browser locale decide the hour-cycle; the SPA reads\nthis to drive its central date/time formatter."},"time_zone":{"type":"string","description":"Web-UI timezone preference (migration 0170): `auto` (browser zone) or\n`profile` (render in `tz`). The SPA resolves it against `tz`."},"title":{"type":["string","null"]},"tz":{"type":["string","null"]},"user_id":{"type":"string","format":"uuid","description":"HQ-internal workspace_users.id."}}},"InboxMessageJson":{"type":"object","required":["id","source_kind","title","body_markdown","ui_strings","delivered_at"],"properties":{"body_markdown":{"type":"string"},"delivered_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"read_at":{"type":["string","null"],"format":"date-time"},"schedule_id":{"type":["string","null"],"format":"uuid"},"schedule_name":{"type":["string","null"],"description":"Set when source_kind = 'schedule_fire' - the originating\nschedule's user-facing label, so the inbox can group + link."},"source_kind":{"type":"string"},"title":{"type":"string"},"ui_strings":{}}},"InstallApiKeyReq":{"type":"object","required":["api_key"],"properties":{"api_key":{"type":"string","description":"The bearer token the agent will use against the MCP backend.\nCaller is responsible for choosing the right scoping (e.g.\nStripe restricted key `rk_test_...`, GitHub PAT, etc.). The\nplatform doesn't validate the token shape - only its presence."},"scope":{"type":["string","null"],"description":"`\"team\"` (default) - credential is shared across the workspace\nand used for every caller. `\"user\"` - credential is private\nto the caller; the resolver only uses it when *this* user is\nthe one making the call (otherwise falls through to the team\nrow if any). Server validates against\n`mcp_servers.allowed_scopes` and rejects scopes the operator\nhasn't permitted for this MCP."}}},"InstallApiKeyResp":{"type":"object","required":["slug","enabled","credential_ref","scope"],"properties":{"credential_ref":{"type":"string","format":"uuid"},"enabled":{"type":"boolean"},"scope":{"type":"string","description":"Echoed `team` / `user` so the caller can confirm the platform\nhonoured their choice rather than silently defaulting."},"slug":{"type":"string"}}},"InstallReq":{"type":"object","required":["slug"],"properties":{"config":{"description":"Optional per-tenant config blob. Stored as-is in\n`tenant_skill_installs.config` - the materializer can read it\nwhen staging the pack."},"slug":{"type":"string"}}},"InstallResp":{"type":"object","required":["slug","enabled"],"properties":{"enabled":{"type":"boolean"},"slug":{"type":"string"}}},"IntegrationDep":{"type":"object","required":["slug","display_name","met"],"properties":{"display_name":{"type":"string"},"met":{"type":"boolean"},"slug":{"type":"string"}}},"IntrospectReq":{"type":"object","required":["token","client_id"],"properties":{"client_id":{"type":"string"},"client_secret":{"type":["string","null"]},"token":{"type":"string"},"token_type_hint":{"type":["string","null"]}}},"IntrospectResp":{"type":"object","required":["active"],"properties":{"active":{"type":"boolean"},"client_id":{"type":["string","null"]},"exp":{"type":["integer","null"],"format":"int64"},"iat":{"type":["integer","null"],"format":"int64"},"scope":{"type":["string","null"]},"sub":{"type":["string","null"]},"token_type":{"type":["string","null"]},"username":{"type":["string","null"]}}},"JoinResp":{"type":"object","required":["joined","channel"],"properties":{"channel":{"$ref":"#/components/schemas/ChannelJson"},"joined":{"type":"boolean"}}},"ListResp":{"type":"object","required":["messages","unread_count","total","page","page_size"],"properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/InboxMessageJson"}},"page":{"type":"integer","format":"int64"},"page_size":{"type":"integer","format":"int64"},"total":{"type":"integer","format":"int64","description":"Total rows matching the unread + search filter, before paging."},"unread_count":{"type":"integer","format":"int64","description":"Unread across the whole inbox (drives the nav badge) - independent\nof the current page / search / sort."}}},"LogoUploadReq":{"type":"object","required":["filename","content_type","body_b64"],"properties":{"body_b64":{"type":"string"},"content_type":{"type":"string"},"filename":{"type":"string"}}},"McpResp":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"array","items":{"type":"string"}}}},"MeResp":{"type":"object","required":["identity","counts","onboarding","out_of_credits","is_operator"],"properties":{"counts":{"$ref":"#/components/schemas/Counts"},"identity":{"$ref":"#/components/schemas/Identity"},"is_operator":{"type":"boolean","description":"True when this caller is an HQ platform operator: an admin of an\n`is_platform` tenant (migration 0182; today `tic`). Drives whether the\nSPA shows the `/operator` back-office nav + route. Presentation only -\nthe real boundary is `platform_admin::require_operator` on `/v1/admin/*`."},"onboarding":{"$ref":"#/components/schemas/OnboardingBlock","description":"Onboarding v2 state (docs/onboarding-v2.md). The web-ui router reads\nthis to enforce the hard gate client-side - the same state the API\nenforces as a 409 - so `/me` is the single source of truth."},"out_of_credits":{"type":"boolean","description":"True when the workspace is out of HQ credits. The SPA mirrors the API's\n402 hard-block by disabling the New-thread / New-ask actions + composer\nand showing an \"upgrade to continue\" prompt. Presentation only - the\nreal boundary is the 402 on the create + send endpoints."},"persona":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Persona"}]}}},"MessageAuthor":{"type":"object","properties":{"avatar_url":{"type":["string","null"]},"name":{"type":["string","null"]}}},"MessageRow":{"type":"object","required":["id","direction","kind","payload","seq","created_at"],"properties":{"author":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MessageAuthor","description":"Resolved author for multi-party threads (Slack/Teams): name + public\navatar URL, looked up from `workspace_users` by the message's\n`identity.sub`. `None` for web/extension owners + unknown users -> the\nfrontend falls back to its own current-user rendering."}]},"created_at":{"type":"string","format":"date-time"},"direction":{"type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"payload":{},"seq":{"type":"integer","format":"int64"},"turn_id":{"type":["string","null"],"format":"uuid"}}},"ModelOption":{"type":"object","required":["slug","display_name","runtime_profile","vendor","is_default"],"properties":{"display_name":{"type":"string"},"family":{"type":["string","null"]},"is_default":{"type":"boolean"},"runtime_profile":{"type":"string"},"slug":{"type":"string"},"vendor":{"type":"string"}}},"ModelsResp":{"type":"object","required":["models"],"properties":{"models":{"type":"array","items":{"$ref":"#/components/schemas/ModelOption"}}}},"NotificationItem":{"type":"object","required":["id","kind","title","severity","status","created_at"],"properties":{"body":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"cta":{},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"severity":{"type":"string"},"status":{"type":"string"},"title":{"type":"string"}}},"NotificationsResp":{"type":"object","required":["items","unread"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NotificationItem"}},"unread":{"type":"integer","format":"int64","description":"How many of the returned-scope rows are still unread - drives the\nbell badge in the SPA."}}},"NudgeBody":{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}},"NudgeResp":{"type":"object","required":["buffered","pending_count"],"properties":{"buffered":{"type":"boolean"},"pending_count":{"type":"integer","minimum":0}}},"OkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"OnboardingBlock":{"type":"object","required":["track","unread_notifications"],"properties":{"business_domain":{"type":["string","null"]},"completed_at":{"type":["string","null"],"format":"date-time","description":"Non-null once the wizard has completed; null => the hard gate is active."},"domain_confirmed_at":{"type":["string","null"],"format":"date-time","description":"Non-null once the admin confirmed the business domain; null => profiling\nhas not been allowed to run yet."},"track":{"type":"string","description":"`slack` / `teams` / `web` - which wizard track the SPA renders."},"unread_notifications":{"type":"integer","format":"int64","description":"Unread in-app notifications for this caller - drives the bell badge."}}},"OwnerJson":{"type":"object","required":["user_id","schedule_count"],"properties":{"display_name":{"type":["string","null"]},"external_id":{"type":["string","null"]},"schedule_count":{"type":"integer","format":"int64"},"user_id":{"type":"string","format":"uuid"}}},"OwnersResp":{"type":"object","required":["owners"],"properties":{"owners":{"type":"array","items":{"$ref":"#/components/schemas/OwnerJson"}}}},"PatchMeReq":{"type":"object","properties":{"time_format":{"type":["string","null"],"description":"`auto` / `12h` / `24h` - the web-UI clock preference (migration 0169)."},"time_zone":{"type":["string","null"],"description":"`auto` / `profile` - the web-UI timezone preference (migration 0170)."}}},"PatchReq":{"type":"object","properties":{"allow_guest_signin":{"type":["boolean","null"]},"consent_mode":{"type":["string","null"]},"corpus_history_days":{"type":["integer","null"],"format":"int32"},"nudge_end_hour":{"type":["integer","null"],"format":"int32"},"nudge_fallback_lang":{"type":["string","null"],"description":"Empty string clears (revert to derived/default); absent leaves unchanged."},"nudge_start_hour":{"type":["integer","null"],"format":"int32"},"nudge_tz":{"type":["string","null"],"description":"Empty string clears (revert to derived/default); absent leaves unchanged."},"proactive_nudges_enabled":{"type":["boolean","null"]},"profile_mode":{"type":["string","null"]},"signin_mode":{"type":["string","null"]}}},"PatchUserReq":{"type":"object","properties":{"admin_access":{"type":["string","null"],"description":"`default` | `granted` | `revoked`. Sets the user's tenant-admin role,\noverriding the Slack `is_admin` flag (migration 0132)."},"signin_access":{"type":["string","null"],"description":"`default` | `allowed` | `denied`. Sets the user's HQ sign-in access."}}},"Persona":{"type":"object","required":["profile","generated_at","message_count","prompt_version"],"properties":{"generated_at":{"type":"string","format":"date-time"},"last_message_at":{"type":["string","null"],"format":"date-time","description":"Timestamp of the latest message included."},"message_count":{"type":"integer","format":"int64","description":"How many messages this distillation considered."},"model":{"type":["string","null"],"description":"Model that produced the doc (e.g. `claude-haiku-4-5`)."},"profile":{"description":"The distilled profile JSON. Shape:\n  { topics:[...], style:{tone,length,emoji_rate,punctuation},\n    role_summary:\"...\", active_hours:{start,end,tz},\n    top_channels:[...], top_collaborators:[...],\n    recurring_themes:[...] }"},"prompt_version":{"type":"integer","format":"int32"}}},"ProfileResp":{"type":"object","required":["overridden","detected"],"properties":{"brand_palette":{},"business_domain":{"type":["string","null"],"description":"Currently-set domain on the tenants row. May differ from\n`crawl.domain` if the admin changed it after the last crawl."},"crawl_domain":{"type":["string","null"]},"crawl_status":{"type":["string","null"]},"detected":{"type":"array","items":{"$ref":"#/components/schemas/DetectedSignal"},"description":"Technology signals detected for the tenant, highest-confidence\nfirst. Powers the read-only \"Detected technology\" section."},"dm_sent_at":{"type":["string","null"],"format":"date-time"},"enrichment":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Enrichment","description":"Company enrichment learned by profiling (migration 0155).\n`None` when none of industry/segment/size_hint are set."}]},"fetched_at":{"type":["string","null"],"format":"date-time"},"fonts":{},"last_error":{"type":["string","null"]},"logo_artifact_id":{"type":["string","null"],"format":"uuid"},"logo_url":{"type":["string","null"]},"overridden":{"type":"array","items":{"type":"string"},"description":"Which fields are currently admin-overridden. Powers the\n\"Use auto\" affordance in the UI - only the overridden fields\nget a reset-button."},"pages_crawled":{"type":["integer","null"],"format":"int32"},"summary":{"type":["string","null"]},"summary_confidence":{"type":["number","null"],"format":"float"}}},"PublicDeleteResp":{"type":"object","required":["surface_id","outcome"],"properties":{"outcome":{"type":"string"},"surface_id":{"type":"string","format":"uuid"}}},"PublicRestoreResp":{"type":"object","required":["surface_id","outcome"],"properties":{"outcome":{"type":"string"},"surface_id":{"type":"string","format":"uuid"}}},"PutReq":{"type":"object","properties":{"brand_palette":{},"business_domain":{"type":["string","null"],"description":"Set or clear the crawled domain. Setting to a different\nvalue than current also enqueues an immediate refresh; this\nis the admin-edit-domain path."},"fonts":{},"logo_url":{"type":["string","null"]},"summary":{"type":["string","null"],"description":"Admin-supplied summary override. When set, the next agent\nturn reads this instead of the crawler's output."}}},"QuarantineRow":{"type":"object","required":["id","from_addr","to_addr","reason","sentio_message_id","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"from_addr":{"type":"string"},"id":{"type":"string","format":"uuid"},"llm_category":{"type":["string","null"]},"llm_summary":{"type":["string","null"]},"reason":{"type":"string"},"sentio_message_id":{"type":"string","format":"uuid"},"to_addr":{"type":"string"}}},"ReadAllResp":{"type":"object","required":["updated"],"properties":{"updated":{"type":"integer","format":"int64","minimum":0}}},"RecItem":{"type":"object","required":["id","kind","target_slug","confidence","status"],"properties":{"category":{"type":["string","null"],"description":"Catalog category."},"confidence":{"type":"number","format":"float"},"description":{"type":["string","null"],"description":"Catalog description."},"display_name":{"type":["string","null"],"description":"Friendly catalog name (NULL when the slug is not in the catalog - the UI\nthen falls back to the raw slug)."},"icon_url":{"type":["string","null"],"description":"Origin-served icon URL for the catalog entry, mirroring the catalog GET\n(`/v1/mcp/catalog/{slug}/icon?v=<md5>` for integrations,\n`/v1/skills/catalog/{slug}/icon?v=<md5>` for skills); `None` when the\nentry has no icon, so the UI falls back to a monogram from display_name."},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"priority":{"type":["string","null"],"description":"LLM-reasoned priority (high | medium | low) when the rec came from the\nreasoning stage; null for plain rule-bridge recs (migration 0155)."},"rationale":{"type":["string","null"],"description":"The model's reasoning for suggesting this - shown so the user sees why."},"reason":{"type":["string","null"]},"status":{"type":"string"},"target_slug":{"type":"string"}}},"RefreshResp":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}}},"RenameConvReq":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"New title. Trimmed; empty/whitespace reverts the conversation to\nauto-derived (clears the title + the custom flag so the worker\nre-derives on the next turn)."}}},"RenameConvResp":{"type":"object","required":["conversation_id","title_is_custom"],"properties":{"conversation_id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"title_is_custom":{"type":"boolean"}}},"ReplaceMcpReq":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"array","items":{"type":"string"}}}},"ReplaceSkillsReq":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"array","items":{"type":"string"}}}},"ReplyReq":{"type":"object","required":["text"],"properties":{"html":{"type":["string","null"]},"text":{"type":"string"}}},"ReviewCounts":{"type":"object","required":["awaiting_review","unrouted","quarantined"],"properties":{"awaiting_review":{"type":"integer","format":"int64"},"quarantined":{"type":"integer","format":"int64"},"unrouted":{"type":"integer","format":"int64"}}},"ReviewQueues":{"type":"object","required":["awaiting_review","unrouted","quarantined","counts"],"properties":{"awaiting_review":{"type":"array","items":{"$ref":"#/components/schemas/ActivityRow"}},"counts":{"$ref":"#/components/schemas/ReviewCounts"},"quarantined":{"type":"array","items":{"$ref":"#/components/schemas/QuarantineRow"},"description":"Admin-only; empty for a non-admin caller."},"unrouted":{"type":"array","items":{"$ref":"#/components/schemas/ActivityRow"},"description":"Admin-only; empty for a non-admin caller."}}},"RevokeReq":{"type":"object","required":["token","client_id"],"properties":{"client_id":{"type":"string"},"client_secret":{"type":["string","null"]},"token":{"type":"string"},"token_type_hint":{"type":["string","null"]}}},"RevokeResp":{"type":"object","required":["revoked"],"properties":{"revoked":{"type":"boolean"}}},"RevokedResp":{"type":"object","required":["revoked","tokens_revoked"],"properties":{"revoked":{"type":"boolean"},"tokens_revoked":{"type":"integer","format":"int64","description":"How many live tokens issued by this app to the caller were revoked.","minimum":0}}},"ScheduleJson":{"type":"object","required":["id","owner_user_id","name","trigger","args","state","fire_count","failure_count","consecutive_failure_count"],"properties":{"args":{},"consecutive_failure_count":{"type":"integer","format":"int32"},"description":{"type":["string","null"]},"failure_count":{"type":"integer","format":"int32"},"fire_count":{"type":"integer","format":"int32"},"id":{"type":"string","format":"uuid"},"last_fire_at":{"type":["string","null"],"format":"date-time"},"last_fire_lag_ms":{"type":["integer","null"],"format":"int64"},"last_fire_status":{"type":["string","null"]},"last_run_conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation backing this schedule's most recent run, if any.\nScheduled-run threads are hidden from the main conversation list;\nthe Schedules page uses this to offer a read-only \"last run\" link."},"name":{"type":"string"},"next_fire_at":{"type":["string","null"],"format":"date-time"},"owner_display_name":{"type":["string","null"]},"owner_external_id":{"type":["string","null"]},"owner_user_id":{"type":"string","format":"uuid"},"state":{"type":"string"},"trigger":{}}},"ScopeOption":{"type":"object","required":["scope","description","requires_admin"],"properties":{"description":{"type":"string","description":"What granting it lets the token do (consent-screen phrasing)."},"requires_admin":{"type":"boolean","description":"True when minting this scope needs the admin role."},"scope":{"type":"string","description":"The scope string to request, e.g. `documents:read`."}}},"SettingsResp":{"type":"object","required":["source_id","source_kind","profile_mode","consent_mode","corpus_history_days","signin_mode","allow_guest_signin","proactive_nudges_enabled","nudge_start_hour","nudge_end_hour"],"properties":{"allow_guest_signin":{"type":"boolean","description":"Whether Slack guests (single-/multi-channel) may sign in. Default\n`false` - guests are blocked unless an admin flips this on (or\nexplicitly allows an individual guest under Users)."},"consent_mode":{"type":"string","description":"`admin_only` (default - the admin opts in for the whole tenant)\nor `per_user` (each user must grant consent before distillation\nconsiders their messages)."},"corpus_history_days":{"type":"integer","format":"int32","description":"How far back the corpus crawler ingests messages. 30–365."},"nudge_end_hour":{"type":"integer","format":"int32","description":"Local hour the daily nudge stops, exclusive (1–24, default 18)."},"nudge_fallback_lang":{"type":["string","null"],"description":"BCP-47 language HQ falls back to when a channel is too quiet to detect\none. `null` = Slack workspace locale, else English."},"nudge_start_hour":{"type":"integer","format":"int32","description":"Local hour the daily nudge may start (0–23, default 9)."},"nudge_tz":{"type":["string","null"],"description":"IANA timezone the daily nudge schedules in (e.g. `Europe/Stockholm`).\n`null` = derive from the connected workspace, else UTC. Set this for a\nweb-only tenant with no Slack to derive a tz from."},"proactive_nudges_enabled":{"type":"boolean","description":"Whether HQ proactively nudges channels - the one-time welcome plus\ncapped daily \"you could connect/turn on X\" messages. Default `true`;\nadmins opt out here."},"profile_mode":{"type":"string","description":"`enabled` / `disabled`. When `enabled`, the profile worker runs\nevery tick and distils a per-user persona doc from the\n`messages_corpus` table."},"signin_mode":{"type":"string","description":"Who in the connected workspace may sign in to HQ (migration 0129):\n`open` (default - any workspace member, minus explicit denies) or\n`approval` (allowlist - only users an admin has explicitly allowed)."},"source_id":{"type":"string","format":"uuid","description":"`workspace_corpus_sources.id` for the (single) connected source.\nSurfaced so the UI knows what it's editing; the PATCH endpoint\nscopes by tenant slug, not by id."},"source_kind":{"type":"string","description":"Connector kind - `slack` / `teams` / `manual`. Read-only."},"source_workspace":{"type":["string","null"],"description":"Display name of the connected workspace (e.g. \"Truespar\"). Read-only."}}},"ShareReq":{"type":"object","properties":{"ttl_secs":{"type":["integer","null"],"format":"int64","description":"Seconds the signed URL stays valid for. None defaults to\n7 days; clamped to the [1 min, 30 day] inclusive range.\nAnything outside that range is rejected so a typo in the\nSPA can't mint a year-long link."}}},"ShareResp":{"type":"object","required":["surface_id","url","exp"],"properties":{"exp":{"type":"integer","format":"int64","description":"Unix-ts seconds; the SPA renders \"expires …\" using this."},"surface_id":{"type":"string","format":"uuid"},"url":{"type":"string","description":"Tokenized URL the user copies. Valid until `exp`."}}},"SkillContentResp":{"type":"object","required":["slug","display_name","profile","trust_tier","editable","files"],"properties":{"description":{"type":["string","null"]},"display_name":{"type":"string"},"editable":{"type":"boolean"},"files":{"type":"array","items":{"$ref":"#/components/schemas/SkillFileOut"}},"profile":{"type":"string"},"slug":{"type":"string"},"trust_tier":{"type":"string"}}},"SkillFileOut":{"type":"object","required":["path","content","is_external"],"properties":{"content":{"type":"string","description":"UTF-8 text for inline files; empty for is_external (Trove-backed)\nblobs - those are large assets, not meant to render in the viewer."},"is_external":{"type":"boolean"},"path":{"type":"string"}}},"SkillsResp":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"array","items":{"type":"string"}}}},"SlackState":{"type":"object","required":["connected"],"properties":{"connected":{"type":"boolean"},"team_name":{"type":["string","null"]}}},"StatusResp":{"type":"object","required":["completed","domain_confirmed","track"],"properties":{"business_domain":{"type":["string","null"]},"completed":{"type":"boolean"},"domain_confirmed":{"type":"boolean"},"track":{"type":"string","description":"Derived from the tenant's corpus source kind: `slack` / `teams` /\n`web`. Drives which wizard track the SPA renders."}}},"SubmitMessageBody":{"type":"object","description":"Body for the 202-style submit endpoint. A slim subset of\n[`PostMessageBody`]: agent / tenant / channel are derived from\nthe conversation row, so the caller never has to know them.","required":["text"],"properties":{"attachments":{"type":"array","items":{"$ref":"#/components/schemas/AttachmentInput"},"description":"File attachments (base64-encoded). Same shape as the inline\nendpoint; carried through to TurnPayload.attachments."},"browser_session":{"type":"boolean","description":"True iff this is a local-browser turn (the extension's \"Use my browser\"\nmode). Fails fast if the user's browser isn't connected, then scopes the\nturn to `computer_*` + injects the browser prompt via the per-turn\n`browser_session` Forge flag - exactly like `POST /v1/agent-browser/ask`,\nbut on the streaming path so the `computer_*` tool calls render live."},"idempotency_key":{"type":["string","null"],"description":"Optional dedup token. Same semantics as the inline endpoint:\nre-POST with the same key inside the 24h TTL is a no-op and\nreturns `idempotent: true`. Caller should generate a UUID\nper logical submit (e.g. per Studio chat-send button click)."},"identity":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value","description":"Free-form identity claims from the caller. PAT auth (#729)\nwill replace this with auth-derived chain; for now the\ntrust boundary is network-level reach to controlplane."}]},"text":{"type":"string"},"turn_source":{"type":["string","null"],"description":"Reply-to-source channel for this turn. Defaults to `\"web\"`; the browser\nextension (#729) sets `\"extension\"` so artifacts/replies aren't pushed\nback into a Slack thread the conv may have been born in. Drives\n`conversations.last_turn_channel` via `turn.rs`."}}},"SubmitResponse":{"type":"object","required":["accepted","stream_url","idempotent"],"properties":{"accepted":{"type":"boolean"},"idempotent":{"type":"boolean","description":"`true` when the Idempotency-Key collided with a prior\nsubmit inside the 24h dedup window. The caller should\nattach to `stream_url` to read the original turn's events\nfrom the journal; no new turn was spawned."},"stream_url":{"type":"string"}}},"SuggestResp":{"type":"object","required":["instructions"],"properties":{"instructions":{"type":"string","description":"Claude's draft. Returned as plain text; caller saves via a\nfollow-up PATCH /v1/agents/{id} {\"instructions\": \"...\"}\nafter a human review pass. Never auto-persisted."}}},"SurfaceLite":{"type":"object","required":["id","slug","state","declared_port","run_mode","auth_mode","url","dev_url"],"properties":{"auth_mode":{"type":"string","description":"\"signed_url\" or \"public\". The Studio Share modal reads this\nto render the visibility toggle and decide whether the bare\nURL is shareable or whether a tokenized URL has to be\nminted on demand."},"declared_port":{"type":"integer","format":"int32"},"dev_url":{"type":"string","description":"Signed URL on the owner-only **dev embed** host\n(`dev-c-<slug>-<tenant>`) that the Studio iframe loads. Same\nbackend app; the `dev-` host is where the edge serves the real\ndevtools runtime (the share `url` gets an empty stub). Always\nsigned (the dev host is always HMAC-gated, even when the surface\nis public). Empty string when no HMAC secret is loaded."},"id":{"type":"string","format":"uuid"},"run_mode":{"type":"string"},"slug":{"type":"string"},"state":{"type":"string"},"url":{"type":"string","description":"Shareable URL: the bare public URL (`auth_mode=public`) or a\nsigned plain-host URL (`signed_url`). This is what the Share\nmodal hands out and \"open in new tab\" points at - the\nvisitor-facing view, which never loads the Studio devtools."}}},"TeamsState":{"type":"object","required":["connected"],"properties":{"connected":{"type":"boolean"},"org_name":{"type":["string","null"]}}},"TokenReq":{"type":"object","required":["grant_type"],"properties":{"client_id":{"type":["string","null"]},"client_secret":{"type":["string","null"],"description":"Confidential-client secret (client_secret_post). Public clients omit it\nand rely on PKCE."},"code":{"type":["string","null"]},"code_verifier":{"type":["string","null"]},"grant_type":{"type":"string"},"redirect_uri":{"type":["string","null"]},"refresh_token":{"type":["string","null"]}}},"TokenResp":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope"],"properties":{"access_token":{"type":"string"},"expires_in":{"type":"integer","format":"int64"},"refresh_token":{"type":"string"},"scope":{"type":"string","description":"Space-delimited granted scopes (RFC 6749 §5.1)."},"token_type":{"type":"string"}}},"TokenView":{"type":"object","required":["id","name","kind","scopes","created_at"],"properties":{"created_at":{"type":"string"},"expires_at":{"type":["string","null"]},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"last_used_at":{"type":["string","null"]},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}}},"TopupReq":{"type":"object","required":["pack_slug"],"properties":{"pack_slug":{"type":"string"}}},"UpdateInput":{"type":"object","required":["trigger","args"],"properties":{"args":{"type":"object"},"description":{"type":["string","null"],"description":"`null` clears, omitted keeps current."},"name":{"type":["string","null"]},"trigger":{"type":"object"}}},"UpdateReq":{"type":"object","properties":{"aliases":{"type":["array","null"],"items":{"type":"string"}},"avatar_url":{"type":["string","null"],"description":"`Some(Some(url))` sets, `Some(None)` clears, `None` leaves\nunchanged. Same explicit-null-vs-absent shape as description."},"cautions":{"type":["array","null"],"items":{"type":"string"}},"description":{"type":["string","null"],"description":"Explicit `null` clears the description; absent leaves it unchanged."},"display_name":{"type":["string","null"]},"instructions":{"type":["string","null"],"description":"Same explicit-null-vs-absent shape as description - admin\ncan clear the per-agent prompt (revert to platform baseline)\nvs. leave it unchanged."},"languages":{"type":["array","null"],"items":{"type":"string"}},"model":{"type":["string","null"],"description":"`Some(Some(slug))` sets the model, `Some(None)` clears to the runtime\ndefault, `None` leaves unchanged. Validated against the registry for\nthe effective runtime (billing C)."},"runtime_profile":{"type":["string","null"]},"slug":{"type":["string","null"],"description":"New routing handle. On rename, the old slug auto-flips into\n`aliases` so existing `@hq <old>` references in chat history\ncontinue to route correctly. Same validation as on create\n(charset, reserved-token blocklist, collision check)."},"tone":{"type":["string","null"],"description":"Personality knobs - same explicit-null-vs-absent shape for\ntone + verbosity (single-value enums). `cautions` and\n`languages` are arrays: Some(vec) replaces, None leaves."},"verbosity":{"type":["string","null"]}}},"UploadBody":{"oneOf":[{"type":"object","description":"Inline markdown: server wraps it as `SKILL.md` in a single-file\ntarball. Covers the \"paste a paragraph or two\" case.","required":["body_markdown","kind"],"properties":{"body_markdown":{"type":"string"},"kind":{"type":"string","enum":["markdown"]}}},{"type":"object","description":"Multi-file pack: caller supplies the directory contents as a\nlist of (path, base64) entries. Must include exactly one\n`SKILL.md` at the root.","required":["files","kind"],"properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/UploadFile"}},"kind":{"type":"string","enum":["files"]}}}]},"UploadFile":{"type":"object","required":["path","content_base64"],"properties":{"content_base64":{"type":"string","description":"Standard base64 (no URL-safe variant) of the file's bytes.\nKeep payloads small - combined raw size is capped at 256 KiB."},"path":{"type":"string","description":"Relative path inside the pack (e.g. `SKILL.md`, `examples/x.md`).\nForward slashes only; no leading slash, no `..` segments."}}},"UploadReq":{"type":"object","required":["filename","content_type","body_b64"],"properties":{"body_b64":{"type":"string","description":"Base64-encoded payload. 16 MiB cap on the JSON body limits raw\nto ~12 MiB - the multipart streaming path lands in pass 2."},"caption":{"type":["string","null"]},"category":{"type":["string","null"],"description":"Free-form user category; the auto-categoriser may overwrite\nthis in a follow-up worker pass once we have Haiku wired."},"channel_id":{"type":["string","null"],"format":"uuid","description":"Required iff `scope == \"channel\"`."},"content_type":{"type":"string"},"filename":{"type":"string"},"scope":{"type":"string","description":"`private` | `channel` | `team`. Defaults to `private`."},"tags":{"type":"array","items":{"type":"string"}}}},"UploadResp":{"type":"object","required":["document_id","download_url","sha256","size_bytes","deduplicated"],"properties":{"deduplicated":{"type":"boolean","description":"True iff this hashed identical to an existing document in this\ntenant - we still record the new metadata row but the Trove\nobject is shared. The UI surfaces this as a \"saved (dedup'd\nagainst existing copy)\" hint."},"document_id":{"type":"string","format":"uuid"},"download_url":{"type":"string"},"enriched":{"type":"boolean","description":"True iff this was a same-owner/same-scope dedup AND the caller\nsupplied new metadata (tags/caption/category) that we actually\nmerged onto the existing row. Lets the UI say \"already in your\nlibrary - details updated\" honestly vs a bare \"already there\"."},"sha256":{"type":"string"},"size_bytes":{"type":"integer","format":"int64","minimum":0}}},"UrlResp":{"type":"object","required":["url"],"properties":{"url":{"type":"string"}}},"UserRow":{"type":"object","required":["id","external_user_id","source_kind","is_admin","is_bot","is_deleted","profile_consent","is_guest","signin_access","admin_access","origin"],"properties":{"admin_access":{"type":"string","description":"Tenant-admin role override (migration 0132): `default` (follow the\nSlack `is_admin` flag), `granted` (admin regardless of Slack), or\n`revoked` (never admin). Effective admin = granted OR (default AND\nis_admin), never when revoked."},"display_name":{"type":["string","null"],"description":"Slack handle (\"markus\", \"jens\"). What `@-mentions` resolve to."},"email":{"type":["string","null"]},"external_user_id":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_bot":{"type":"boolean"},"is_deleted":{"type":"boolean"},"is_guest":{"type":"boolean","description":"Whether the transport flags this user as a restricted guest\n(Slack single-/multi-channel). NULL (not yet discovered) renders\nas `false`."},"last_seen_at":{"type":["string","null"],"format":"date-time"},"locale":{"type":["string","null"]},"origin":{"type":"string","description":"`slack` | `teams` | `email` - how the identity entered the tenant. Email\nusers are invited + sign in via magic link (migration 0133). Combined\nwith a null `last_seen_at` the UI shows an \"Invited\" (pending) state."},"profile_consent":{"type":"string"},"real_name":{"type":["string","null"],"description":"Full human name (\"Markus Persson\"). The admin table prefers\nthis in the primary column so users aren't shown as handles."},"signin_access":{"type":"string","description":"HQ sign-in access for this user: `default` (follows the workspace\nsign-in mode + guest policy), `allowed` (explicitly let in), or\n`denied` (explicitly blocked). See migration 0129."},"source_kind":{"type":"string","description":"`slack` / `teams` / `manual` - the connector that originated\nthis identity."},"source_workspace":{"type":["string","null"],"description":"Display name of the connected workspace (e.g. \"Truespar\")."},"title":{"type":["string","null"]},"tz":{"type":["string","null"]}}},"Value":{},"VerifyResp":{"type":"object","required":["ok","checked"],"properties":{"broken_at_seq":{"type":["integer","null"],"format":"int64","description":"The `seq` where the chain first breaks (None when `ok`)."},"broken_reason":{"type":["string","null"],"description":"`\"content\"` (entry hash doesn't recompute) or `\"linkage\"` (prev_hash\ndoesn't match the predecessor's entry hash)."},"checked":{"type":"integer","format":"int64","description":"How many entries were walked."},"first_seq":{"type":["integer","null"],"format":"int64"},"last_seq":{"type":["integer","null"],"format":"int64"},"ok":{"type":"boolean","description":"True iff every checked entry's hash recomputes AND links to its\npredecessor. A single edited/inserted/removed/reordered row flips this."}}},"VisibilityReqPublic":{"type":"object","required":["visibility"],"properties":{"visibility":{"type":"string","description":"\"private\" or \"public\"."}}},"VisibilityRespPublic":{"type":"object","required":["surface_id","visibility","auth_mode"],"properties":{"auth_mode":{"type":"string"},"surface_id":{"type":"string","format":"uuid"},"visibility":{"type":"string"}}},"WorkspaceMailboxPatch":{"type":"object","description":"PATCH the workspace outbound mailbox: change its localpart (the address all\nagents send from). The localpart edit renames `tenants.outbound_mailbox_localpart`\nAND the system endpoint's localpart in one transaction.\n\nThe workspace mailbox is fire-and-notify ONLY: it never auto-replies. A\nreply to it always files to the Review queue + notifies the initiator, so\n`reply_policy` is fixed to `never_reply` and is NOT configurable here (an\naddress that *answers* mail is a custom inbox, created via create_endpoint).\nThe only knob is the address.","properties":{"localpart":{"type":["string","null"]}}}},"securitySchemes":{"bearer_pat":{"type":"http","scheme":"bearer","bearerFormat":"hq_pat","description":"Personal Access Token. Send as `Authorization: Bearer hq_pat_...`."},"oauth2":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https://app.hq.zone/v1/oauth/authorize","tokenUrl":"https://api.hq.zone/v1/oauth/token","refreshUrl":"https://api.hq.zone/v1/oauth/token","scopes":{"admin":"Administer the workspace (users, settings, integrations)","agents:read":"View the agents in your workspace","agents:write":"Create and configure agents","billing:read":"View usage and billing information","conversations:read":"Read your conversations and their messages","conversations:write":"Start conversations and send messages on your behalf","documents:read":"Read your documents library","documents:write":"Upload and manage documents in your library","memory:read":"Read what the assistant remembers about you","memory:write":"Correct or delete what the assistant remembers","schedules:read":"View your scheduled tasks","schedules:write":"Create and manage scheduled tasks"}}}}}},"tags":[{"name":"me","description":"The signed-in user's own account"},{"name":"conversations","description":"Conversations and their messages"},{"name":"documents","description":"The content-addressed documents library"},{"name":"schedules","description":"Scheduled prompts and recurring tasks"},{"name":"agents","description":"Agents, their skills and integrations"},{"name":"memory","description":"What the assistant remembers (L5 governance)"},{"name":"tokens","description":"Personal Access Token management"},{"name":"billing","description":"Usage and billing"},{"name":"notifications","description":"In-app notification center"},{"name":"admin","description":"Workspace administration"},{"name":"integrations","description":"Workspace integrations (Slack, MCP, skills)"},{"name":"onboarding","description":"New-workspace onboarding wizard"},{"name":"auth","description":"Sign-in, sessions, and OAuth"}]}