{"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/AuditListResp"}}}},"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"],"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/AgentsListResp"}}}}},"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/CreateAgentReq"}}},"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}/avatar/generate":{"post":{"tags":["agents"],"summary":"Generate an agent avatar","description":"`POST /v1/agents/{id}/avatar/generate` - admin-triggered xAI image\ngeneration. The optional `prompt` describes the desired look; it seeds the\nsubject and is wrapped in the house illustration style + safety rails, then\nthe image is stored in Trove and set as the agent's `avatar_url`.\nSynchronous (a few seconds). The cost is absorbed by the platform\n(`provider_usage.paid_by='platform'`). Admin only.","operationId":"generate_avatar","parameters":[{"name":"id","in":"path","description":"Agent id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateAvatarReq"}}},"required":true},"responses":{"200":{"description":"The new avatar URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateAvatarResp"}}}},"404":{"description":"No such agent in this workspace"},"500":{"description":"Generation failed (xAI / Trove)"}},"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/admin/workspace/slug":{"post":{"tags":["admin"],"summary":"Set / rename the workspace handle (admin).","description":"Renames `tenants.slug` (the URL handle) and realiases the email slug to\nmatch, keeping the old email address routing for a continuity window. The\nSPA reloads into `/<new>/`; stale `/<old>/` links self-heal via the\nemail-keyed session. Re-validates atomically; the `tenants.slug` unique\nconstraint is the final arbiter against a race.","operationId":"set_workspace_slug","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSlugReq"}}},"required":true},"responses":{"200":{"description":"Renamed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSlugResp"}}}},"400":{"description":"Invalid, taken, or reserved handle"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/admin/workspace/slug/available":{"get":{"tags":["admin"],"summary":"Check whether a workspace handle is free (admin).","description":"Validates format + reserved words and checks global uniqueness across BOTH\nthe URL registry (`tenants.slug`) and the email registry\n(`tenant_email_slug`), so the handle works for the URL and the email address\nalike. Advisory only - `set_slug` re-checks atomically.","operationId":"workspace_slug_available","parameters":[{"name":"slug","in":"query","description":"candidate workspace handle","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Availability verdict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/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}/deliver":{"post":{"tags":["conversations"],"summary":"Deliver an artifact to a channel","description":"Pushes one of a conversation's delivered artifacts into a Slack channel the\nHQ bot belongs to. Used from the Studio when a file the agent produced in a\nweb conversation needs to reach Slack (the automatic reply-to-source mirror\nonly fires for Slack-born turns). The file is uploaded to Slack natively and\nposted as the conversation's agent with a \"Shared from Studio by <user>\"\nnote. Scoped to the caller's workspace and a conversation they can see;\nreturns 400 when there is no live Slack install or HQ is not in the channel.","operationId":"deliver_artifact","parameters":[{"name":"id","in":"path","description":"Artifact id to deliver","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliverArtifactReq"}}},"required":true},"responses":{"200":{"description":"Delivered to the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliverArtifactResp"}}}},"400":{"description":"No live Slack install, HQ not in the channel, or artifact not deliverable"},"404":{"description":"Artifact not found or not visible to the caller"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/v1/api/artifacts/{id}/download":{"get":{"tags":["conversations"],"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/slack/install":{"get":{"tags":["integrations"],"summary":"Connect Slack to the current workspace","description":"Begins binding a Slack workspace to the caller's EXISTING tenant (the\nalready-signed-in admin's workspace). Unlike the public, invite-gated\n`/get-started/slack` landing - which provisions a brand-new tenant from the\ninstall - this authenticated entry attaches the Slack workspace the admin\nauthorizes to their current tenant. We mint a single-use, short-TTL bind\nticket in Forge carrying this tenant's id, then 303-redirect the browser to\nslack-transport's install flow (same origin) with the ticket nonce;\nslack-transport owns the Slack authorize URL and threads the tenant into the\nOAuth `state` so the callback binds rather than creates. Admin only; this is\na top-level browser navigation, not an XHR.","operationId":"connect_slack_install","responses":{"303":{"description":"Redirect into the Slack install/consent flow"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["agents:write"]},{"oauth2":["agents:write"]}]}},"/v1/api/connect/slack/reconnect":{"get":{"tags":["integrations"],"summary":"Reconnect (re-authorize) Slack for the current workspace","description":"The same bind-ticket flow as `connect_slack_install`, but WITHOUT the\nalready-connected guard: a deliberate re-authorization of the SAME Slack\nworkspace, used to grant scopes added since the last install (new permissions\nonly take effect on re-consent). The OAuth callback's same-tenant rebind arm\nrefreshes the token in place - it never provisions a second workspace, and a\nmismatched team is refused there. Admin only; top-level browser navigation.","operationId":"connect_slack_reconnect","responses":{"303":{"description":"Redirect into the Slack re-auth/consent flow"},"403":{"description":"Admin role required"}},"security":[{"bearer_pat":["agents:write"]},{"oauth2":["agents:write"]}]}},"/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/connect/teams/package":{"get":{"tags":["integrations"],"summary":"Download the Teams app package","description":"Streams the HQ Microsoft Teams app package (manifest zip) for an admin to\nupload to their Teams org catalog / sideload. The file path is configured by\n`HQ_TEAMS_APP_PACKAGE`; unconfigured or missing -> 404. Admin only. The\npackage is the same for every org (the bind happens via the Microsoft\nsign-in claim, not a personalized package). See docs/teams-microsoft-signin.md.","operationId":"connect_teams_package","responses":{"200":{"description":"Teams app package (application/zip)"},"403":{"description":"Admin role required"},"404":{"description":"No package configured"}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/v1/api/connect/teams/start":{"get":{"tags":["integrations"],"summary":"Connect Microsoft Teams to the current workspace","description":"Begins binding a Microsoft Teams org to the caller's EXISTING tenant. Teams\nhas no install redirect HQ controls (unlike Slack), so this starts a\nverified \"Sign in with Microsoft\" (Entra OIDC): the callback records a\n`teams_bind_claims` row tying the admin's AAD org (`tid`) + user (`oid`) to\nthis HQ tenant, and a later Teams app install in that org binds to this\ntenant when the installer matches. Admin only; top-level browser navigation,\nnot an XHR. See docs/teams-microsoft-signin.md.","operationId":"connect_teams_start","responses":{"303":{"description":"Redirect into the Microsoft sign-in flow"},"403":{"description":"Admin role required"},"500":{"description":"Microsoft sign-in not configured"}},"security":[{"bearer_pat":["agents:write"]},{"oauth2":["agents:write"]}]}},"/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}/live-activity":{"post":{"tags":["notifications"],"summary":"Register a Live Activity push token","description":"Called by the iOS app when it starts a Live Activity for a running turn in\nthis conversation. Idempotent per `(conversation, token)`; tokens rotate\nwithin an activity, so multiple rows per conversation are normal. Rows are\npruned by the push sender after the terminal update.","operationId":"register_live_activity","parameters":[{"name":"id","in":"path","description":"Conversation id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterLiveActivityReq"}}},"required":true},"responses":{"200":{"description":"Registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveActivityOkResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/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/device-tokens":{"post":{"tags":["notifications"],"summary":"Register a device for push","description":"Upserts the caller's APNs device token for the current workspace. Idempotent\nper `(token, tenant)`: a re-register from the same device updates the owning\nuser, platform, bundle id, environment and refreshes `last_seen_at`.","operationId":"register_device_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDeviceReq"}}},"required":true},"responses":{"200":{"description":"Registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenOkResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/device-tokens/{token}":{"delete":{"tags":["notifications"],"summary":"Unregister a device token","description":"Removes the caller-tenant's registration for this device token (called on\nsign-out). Scoped to the caller's workspace; APNs feedback prunes the rest.","operationId":"delete_device_token","parameters":[{"name":"token","in":"path","description":"APNs device token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unregistered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenOkResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/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":"folder","in":"query","description":"Filter to documents directly inside this folder.","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"root","in":"query","description":"When true, only documents at the root of their scope (`folder_id IS\nNULL`). Ignored when `folder` is set.","required":false,"schema":{"type":["boolean","null"]}},{"name":"trash","in":"query","description":"When true, return the caller's Trash (their soft-deleted documents)\ninstead of live ones. Overrides scope/folder/root filters.","required":false,"schema":{"type":["boolean","null"]}},{"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/DocumentsListResp"}}}}},"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/DocumentUploadReq"}}},"required":true},"responses":{"200":{"description":"Document saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUploadResp"}}}},"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/DocumentsListResp"}}}}},"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/DocumentUploadResp"}}}},"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":"Moves a document to Trash (soft-delete) by default - the bytes are kept and\n`POST /v1/api/documents/{id}/restore` brings it back. Pass `?purge=true` to\ndelete it permanently; the stored file content is then removed only when no\nother document in the workspace references the same bytes, so deduplicated\ncopies are preserved. Only the document's owner may delete it (others receive\n403; an unknown document returns 404). Returns 204 No Content. Requires the\ndocuments:write scope.","operationId":"delete_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"purge","in":"query","description":"When true, permanently delete (and drop the bytes when no other document\nshares them) instead of moving to Trash. This is \"delete forever\" /\nempty-trash; the default soft-deletes so it can be restored.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"204":{"description":"Trashed (or purged with ?purge=true)"},"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/DocumentPatchReq"}}},"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}/assistant":{"get":{"tags":["documents"],"summary":"Look up the caller's document-assistant thread","description":"Returns the caller's existing assistant conversation for this document + agent\n(so the editor reloads + continues it), or `null` if none yet. Does not create\nanything. Requires documents:read.","operationId":"get_document_assistant","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"agent_id","in":"query","description":"The agent whose thread to look up (the editor picks per the agent picker).","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The thread id, or null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocAssistantResp"}}}},"404":{"description":"Document not found / not visible"}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]},"post":{"tags":["documents"],"summary":"Open (find or create) the document-assistant thread","description":"Returns the caller's assistant conversation for this document + agent, creating\nit (bound to the document, with the caller as owner) on first use. The editor\ncalls this on the first message. Requires conversations:write.","operationId":"open_document_assistant","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocAssistantReq"}}},"required":true},"responses":{"200":{"description":"The thread id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocAssistantResp"}}}},"402":{"description":"Workspace out of credits"},"404":{"description":"Document or agent not found"}},"security":[{"bearer_pat":["conversations:write"]},{"oauth2":["conversations:write"]}]}},"/v1/api/documents/{id}/collab-ticket":{"post":{"tags":["documents"],"summary":"Mint a collaboration websocket ticket for a document","description":"Authenticates the caller (cookie or bearer) and checks that the document is\nvisible to them, then returns a short-lived, single-use, doc-scoped HMAC\nticket plus the same-origin websocket path. The browser opens the\ncollaboration websocket (served by the hq-docs daemon, nginx-fronted on\n443/WSS) carrying the ticket in `Sec-WebSocket-Protocol`. Requires the\ndocuments:read scope; holding documents:write upgrades the ticket's\ncapability to `edit`.","operationId":"mint_collab_ticket","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Collaboration ticket","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollabTicketResp"}}}},"404":{"description":"Not found / not visible to caller"},"500":{"description":"Collaboration signing not configured"}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]}},"/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}/export-pdf":{"post":{"tags":["documents"],"summary":"Render an editor document to PDF via the host LibreOffice service. The editor\nPOSTs the live document's freshly-serialized `.docx` bytes (exactly what's on\nscreen); the host converts them behind the hardened soffice wrapper (shared\nwith the `office_to_pdf` MCP tool) and streams back the PDF as a named\nattachment. The `.docx` download stays client-side - only PDF needs the host\n(LibreOffice is not in the browser).","operationId":"export_pdf","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"The live document's .docx bytes","content":{"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{"schema":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}}}},"required":true},"responses":{"200":{"description":"The document rendered as a PDF (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/documents/{id}/restore":{"post":{"tags":["documents"],"summary":"Restore a trashed document","description":"Brings a soft-deleted document back from Trash (clears `deleted_at`). If the\nfolder it lived in has since been trashed, it is restored to the root of its\nscope instead. Owner-only. Returns the restored document. Requires\ndocuments:write.","operationId":"restore_document","parameters":[{"name":"id","in":"path","description":"Document id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Restored document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRow"}}}},"403":{"description":"Not the owner"},"404":{"description":"Not found in Trash"}},"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}/assign-owner":{"post":{"tags":["admin"],"summary":"Assign a review-reply item to a user","description":"Admin-only. Attributes a `review_reply` item (a reply to the team / default\nsender) to a workspace user, moving it into that user's review queue. Does\nNOT drive a turn or re-inject the reply into agent context - it only sets the\nreviewer. Use it to triage the Unassigned bucket.","operationId":"assign_email_owner","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignOwnerReq"}}},"required":true},"responses":{"200":{"description":"Reviewer assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not a review_reply item, or unknown user"},"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}/reopen":{"post":{"tags":["admin"],"summary":"Reopen a reviewed email reply","description":"Flips a reviewed (`handled`) review-reply item back to the review queue -\nrecovers a mis-clicked Done. Owner-or-admin, and only an item that was a\nreview reply (`review_owner_id` set) and is currently `handled`.","operationId":"reopen_email_reply","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Reopened","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not a reviewed review-reply item"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/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/messages/{id}/reviewed":{"post":{"tags":["admin"],"summary":"Mark a review-reply item reviewed","description":"Clears a `review_reply` item from the review queue (sets it `handled`). The\nowner of the item (or an admin) may clear it, once a human has dealt with the\nreply. Does not touch agent context.","operationId":"review_reply_done","parameters":[{"name":"id","in":"path","description":"Email message id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked reviewed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionOk"}}}},"400":{"description":"Not a review_reply item"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/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/replies":{"get":{"tags":["admin"],"summary":"List the caller's assigned email replies","description":"The member \"Email replies\" surface: replies attributed to the caller\n(review_owner_id), filtered by `reviewed` - false (default) = to-review,\ntrue = already reviewed. Owner-scoped to the caller's own items.","operationId":"list_my_email_replies","parameters":[{"name":"reviewed","in":"query","description":"false (default) = to-review (`review_reply`); true = reviewed (`handled`).","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"description":"The caller's review-owned replies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepliesResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/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/folders":{"get":{"tags":["documents"],"summary":"List folders","description":"Returns every folder the caller can see (their private folders, all team\nfolders, and the folders of channels they belong to), flat - the client\nbuilds the tree from `parent_id`. Each row carries a live `doc_count`.\nRequires documents:read.","operationId":"list_folders","parameters":[{"name":"scope","in":"query","description":"Filter to one scope (`private` / `team` / `channel`); omit for all.","required":false,"schema":{"type":["string","null"]}},{"name":"trash","in":"query","description":"When true, return the caller's trashed folders (the top of each trashed\nsubtree) instead of live ones - the Trash view. Overrides `scope`.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"description":"Folders visible to the caller","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FolderRow"}}}}}},"security":[{"bearer_pat":["documents:read"]},{"oauth2":["documents:read"]}]},"post":{"tags":["documents"],"summary":"Create a folder","description":"Creates a folder in one of the caller's roots. `scope='channel'` requires a\n`channel_id` the caller belongs to; a `parent_id`, if given, must be a\nvisible folder of the same scope. Names are unique within a parent + scope.\nRequires documents:write.","operationId":"create_folder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderCreateReq"}}},"required":true},"responses":{"200":{"description":"Created folder","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderRow"}}}},"400":{"description":"Invalid request"},"409":{"description":"Name already exists here"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/folders/{id}":{"delete":{"tags":["documents"],"summary":"Trash or permanently delete a folder","description":"Soft-deletes a folder and everything under it (descendant folders + the\ndocuments they hold travel to Trash together, like Drive/Dropbox). Restore\nwith `POST /v1/api/folders/{id}/restore`. Pass `?purge=true` to permanently\ndelete an already-trashed folder: the subtree folders + their documents are\nhard-deleted and each document's bytes dropped from storage (dedup-guarded).\nPrivate folders are owner-only; team/channel folders are deletable by any\nmember. Requires documents:write.","operationId":"delete_folder","parameters":[{"name":"id","in":"path","description":"Folder id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"purge","in":"query","description":"When true, permanently delete (and drop the bytes when no other document\nshares them) instead of moving to Trash. This is \"delete forever\" /\nempty-trash; the default soft-deletes so it can be restored.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"204":{"description":"Trashed (or purged with ?purge=true)"},"403":{"description":"Not allowed"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]},"patch":{"tags":["documents"],"summary":"Rename or move a folder","description":"Renames and/or reparents a folder within its scope. A new parent must be a\nvisible folder of the same scope and may not be the folder itself or one of\nits descendants (no cycles). Private folders are owner-only; team/channel\nfolders are editable by any member. Requires documents:write.","operationId":"update_folder","parameters":[{"name":"id","in":"path","description":"Folder id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderPatchReq"}}},"required":true},"responses":{"200":{"description":"Updated folder","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderRow"}}}},"400":{"description":"Invalid request (e.g. cycle)"},"403":{"description":"Not allowed"},"404":{"description":"Not found"},"409":{"description":"Name already exists here"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents:write"]}]}},"/v1/api/folders/{id}/restore":{"post":{"tags":["documents"],"summary":"Restore a trashed folder","description":"Brings a folder and its trashed subtree (descendant folders + their\ndocuments) back from Trash. If the folder's own parent is still trashed, it\nis restored to the root of its scope. Requires documents:write.","operationId":"restore_folder","parameters":[{"name":"id","in":"path","description":"Folder id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Restored folder","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderRow"}}}},"403":{"description":"Not allowed"},"404":{"description":"Not found in Trash"}},"security":[{"bearer_pat":["documents:write"]},{"oauth2":["documents: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/InboxListResp"}}}}},"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/InboxOkResp"}}}}},"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/InboxOkResp"}}}},"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/InboxOkResp"}}}},"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/InboxOkResp"}}}},"404":{"description":"Message not found"}},"security":[{"bearer_pat":[]},{"oauth2":[]}]}},"/v1/api/me":{"get":{"tags":["me"],"summary":"Get the signed-in user","description":"The caller's own self-view, scoped to their identity: an identity block\n(display name, email, locale, timezone, admin/bot flags, connected-workspace\nsource, profile-consent state, and clock/timezone display preferences), an\noptional persona block summarizing the user's distilled profile (`null` if\nnot yet generated), quick counts such as the number of schedules owned, the\nworkspace onboarding state, and an `out_of_credits` flag indicating whether\nthe workspace 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/avatar":{"post":{"tags":["me"],"summary":"Upload my avatar","description":"`POST /v1/api/me/avatar` - the caller uploads their own profile image.\nMirrors the agent-avatar upload: base64 in, decoded + validated locally,\nthen handed to the internal artifacts publisher (Trove), and the resulting\ninternal download URL is stored as the caller's `workspace_users.image_url`.\nSelf-scoped (resolved from the session, never another user). EMAIL-ORIGIN\nonly: connector (Slack/Teams) users get their avatar synced from upstream,\nso a local upload would be clobbered by the next sync - reject rather than\nmislead. Boundary enforced here, not just hidden in the UI.","operationId":"upload_my_avatar","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeAvatarReq"}}},"required":true},"responses":{"200":{"description":"Avatar updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeAvatarResp"}}}},"400":{"description":"Empty / oversized / unsupported image"},"403":{"description":"Avatar is managed by your connected Slack/Teams profile"}},"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/live-activity-start-token":{"post":{"tags":["notifications"],"summary":"Register a Live Activity push-to-start token","description":"iOS 17.2+ push-to-start: a PER-DEVICE token the control-plane can use to\nRAISE a Lock Screen card for a turn the app never started (scheduled runs,\nweb-initiated turns). Long-lived but rotates on reboot/reinstall — the app\nre-registers on every launch; idempotent per `(token, tenant)`.\n\nTo raise a card, a producer enqueues an outbox row on\n`hq.{tenant}.notifications.live_activity` with `event: \"start\"`,\n`recipient_user_id`, `conversation_id`, `title`, `agent_name`, plus the\nusual `phase`/`status_text` — the APNs sender (hq-worker) does the rest.","operationId":"register_live_activity_start_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterLiveActivityReq"}}},"required":true},"responses":{"200":{"description":"Registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveActivityOkResp"}}}}},"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/me/workspaces":{"get":{"tags":["me"],"summary":"List the workspaces the signed-in person can reach","description":"Returns every workspace the caller's verified email can sign into\n(policy-passing - the same `signin_decision` gate as sign-in), with the\nactive (current-session) one flagged. This is the read-only foundation of\nthe cross-tenant workspace switcher (P1); re-scoping the session to another\nworkspace (the active-tenant signed claim) is P2. When the session identity\ncarries no email, only the current workspace is returned.","operationId":"my_workspaces","responses":{"200":{"description":"Workspaces the caller can reach","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspacesResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]}]},"post":{"tags":["me"],"summary":"Create a new workspace","description":"Greenfield-creates a brand-new tenant owned by the caller and switches the\nsession into it - the authenticated, in-product counterpart to the public\nmagic-link `/v1/auth/signup` funnel. Shares the exact same atomic provisioning\n(`signup::provision_email_tenant`), so the new workspace gets the identical\nonboarding flow; the only differences are that the owner is taken from the\ncaller's verified session email (no re-entry, no magic-link round-trip) and\nthe session cookie is re-minted for the new tenant on the way out.\n\nStill invite-gated: an `invite_code` is required and consumed single-use, so\nan authenticated member can't mint workspaces freely. Web-session only - a\nPAT / OAuth token is pinned to one workspace and can't create another.\n\nIdempotency: the single-use invite row is the HARD guard (no double-create\neven under concurrency or a cache outage). An optional `Idempotency-Key`\nheader adds soft lost-response replay - a retry with the same key returns the\nalready-created workspace instead of a spurious \"invite already used\".","operationId":"create_workspace","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceReq"}}},"required":true},"responses":{"200":{"description":"Workspace created; session re-scoped to it (fresh cookie set)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceResp"}}}},"400":{"description":"Web-session-only, or the caller has no verified email to own a workspace"},"403":{"description":"Missing / invalid / used invite, or the account is over its workspace quota"},"409":{"description":"A create with this Idempotency-Key is already in flight"}},"security":[{"cookie_session":[]}]}},"/v1/api/me/workspaces/switch":{"post":{"tags":["me"],"summary":"Switch the active workspace","description":"Re-scopes the session to another workspace the caller can reach (workspace\nswitcher P2). Verifies the caller's verified email has a policy-passing\nmembership in the target - the identical `signin_decision` gate as sign-in,\nso this never grants access a fresh sign-in wouldn't - then re-mints the\nvault-sealed session cookie for the target identity (Pattern 2: instant, no\nSlack re-auth). Returns 200 + a fresh `Set-Cookie`; the SPA reloads into the\nnew workspace. 403 if the caller has no eligible membership in the target.","operationId":"switch_workspace","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchWorkspaceReq"}}},"required":true},"responses":{"200":{"description":"Active workspace switched (fresh session cookie set)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchWorkspaceResp"}}}},"400":{"description":"Web-session-only: a PAT/OAuth token can't switch (its tenant is fixed by the token)"},"403":{"description":"Caller has no eligible membership in the target workspace"}},"security":[{"cookie_session":[]}]}},"/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/NotificationsOkResp"}}}}},"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/NotificationsOkResp"}}}}},"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/runs":{"get":{"tags":["runs"],"summary":"List runs","description":"Lists schedule runs (one row per executed fire) newest-first, with a total for\npagination. scope=mine (default) returns only runs of the caller's own schedules;\nscope=workspace returns all runs in the workspace and is admin only (403 otherwise).\nstatus filters to all (default), active (created/running/suspended), or failed.\nPass schedule_id for one schedule's history - it is gated owner-or-admin (404\notherwise). limit defaults to 50 (max 200). Requires the schedules:read scope.","operationId":"list_runs","parameters":[{"name":"scope","in":"query","description":"`mine` (default) or `workspace` (admin only).","required":false,"schema":{"type":["string","null"]}},{"name":"status","in":"query","description":"`all` (default), `active` (created/running/suspended), or `failed`.","required":false,"schema":{"type":["string","null"]}},{"name":"schedule_id","in":"query","description":"Restrict to one schedule's history. Visibility-gated owner-or-admin.","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"order","in":"query","description":"`newest` (default, started_at DESC) or `oldest` (ASC).","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Runs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunsListResp"}}}},"400":{"description":"Invalid scope/status"},"403":{"description":"scope=workspace requires admin"},"404":{"description":"schedule_id not visible to caller"}},"security":[{"bearer_pat":["schedules:read"]},{"oauth2":["schedules:read"]}]}},"/v1/api/runs/{id}/cancel":{"post":{"tags":["runs"],"summary":"Cancel a run","description":"Cancels a run still in flight (created/running/suspended) - sets it to cancelled,\nstamps finished_at, and drops its pending timers/awakeables. A no-op (ok=false) if\nthe run already finished. The caller must own the run's schedule or be an admin\n(404 if the run isn't visible). Requires the schedules:write scope.","operationId":"cancel_run","parameters":[{"name":"id","in":"path","description":"Run (instance) id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancel result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunsOkResp"}}}},"404":{"description":"Run not found / not visible"}},"security":[{"bearer_pat":["schedules:write"]},{"oauth2":["schedules: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/SchedulesListResp"}}}},"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/SchedulesOkResp"}}}},"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/SchedulesOkResp"}}}},"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/SchedulesOkResp"}}}},"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/SchedulesOkResp"}}}},"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/SchedulesOkResp"}}}},"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/SchedulesOkResp"}}}},"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/schedules/{id}/usage":{"get":{"tags":["schedules"],"summary":"Token + cost consumption for a schedule","description":"Aggregates `conversation_usage` across every fire of this schedule (each fire\nis its own conversation, stamped with `schedule_id`). Returns all-time totals\nplus a capped per-fire series for a trend sparkline. Same visibility gate as\nthe detail endpoint.","operationId":"get_schedule_usage","parameters":[{"name":"id","in":"path","description":"Schedule id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Schedule usage","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleUsageResp"}}}},"404":{"description":"Not found"}},"security":[{"bearer_pat":["schedules:read"]},{"oauth2":["schedules:read"]}]}},"/v1/api/search":{"get":{"tags":["search"],"summary":"Search conversations","description":"Full-transcript hybrid search (lexical BM25 + dense vector, reciprocal-rank\nfused, then cross-encoder reranked) over the caller's conversations within\ntheir workspace. Returns message-level hits - each carries the conversation,\nthe matched message's sequence number (a jump-to-message deep link), a\nhighlighted snippet, and a relevance score - newest-relevance first. Scoped\nto conversations the caller participates in. Cursor-paginated.","operationId":"search_conversations","parameters":[{"name":"q","in":"query","description":"Free-text query. Searches the full message transcript, not just titles.","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Max hits to return (default 20, max 50).","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"cursor","in":"query","description":"Opaque forward-pagination cursor from a previous response.","required":false,"schema":{"type":["string","null"]}},{"name":"channel_kind","in":"query","description":"Restrict to one channel: `slack` | `teams` | `web` | `api` | `cli` | `email`.","required":false,"schema":{"type":["string","null"]}},{"name":"agent","in":"query","description":"Restrict to one agent (slug).","required":false,"schema":{"type":["string","null"]}},{"name":"mode","in":"query","description":"Lane filter: `fast` (quick-ask threads) | `standard` | `all` (default all).","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Ranked message-level search hits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResp"}}}}},"security":[{"bearer_pat":["conversations:read"]},{"oauth2":["conversations:read"]}]}},"/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/SettingsPatchReq"}}},"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/tables":{"get":{"tags":["tables"],"summary":"List tables","operationId":"list_tables","responses":{"200":{"description":"Tables"},"503":{"description":"Tables not configured"}},"security":[{"bearer_pat":["tables:read"]},{"oauth2":["tables:read"]}]},"post":{"tags":["tables"],"summary":"Create a table","operationId":"create_table","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewTableReq"}}},"required":true},"responses":{"200":{"description":"Created table"},"400":{"description":"Invalid spec"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/tables/{id}":{"get":{"tags":["tables"],"summary":"Get a table with its fields","operationId":"get_table","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Table + fields"},"404":{"description":"Not found"}},"security":[{"bearer_pat":["tables:read"]},{"oauth2":["tables:read"]}]},"delete":{"tags":["tables"],"summary":"Delete a table","operationId":"delete_table","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Applied or queued for approval"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/tables/{id}/export.csv":{"get":{"tags":["tables"],"summary":"Export a table as CSV","description":"Streams the table's live rows as a UTF-8 CSV attachment (RFC 4180, BOM for\nExcel, formula-injection defanged). Keyset-paged server-side so the response\nis constant-memory and never capped at the grid's page size. Full export\n(no filters) in v1. Requires the tables:read scope.","operationId":"export_table_csv","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"CSV of all rows (streamed attachment)"},"404":{"description":"Not found"},"503":{"description":"Tables not configured"}},"security":[{"bearer_pat":["tables:read"]},{"oauth2":["tables:read"]}]}},"/v1/api/tables/{id}/fields":{"post":{"tags":["tables"],"summary":"Add a field to a table","operationId":"add_field","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewFieldReq"}}},"required":true},"responses":{"200":{"description":"Created field"},"400":{"description":"Invalid field"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/tables/{id}/fields/{field_id}":{"delete":{"tags":["tables"],"summary":"Drop a field","operationId":"drop_field","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"field_id","in":"path","description":"Field id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Applied or queued for approval"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/tables/{id}/rows":{"post":{"tags":["tables"],"summary":"Insert rows","operationId":"insert_rows","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertRowsReq"}}},"required":true},"responses":{"200":{"description":"Inserted row ids"},"400":{"description":"Invalid rows"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/tables/{id}/rows/query":{"post":{"tags":["tables"],"summary":"Query rows","operationId":"query_rows","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RowQueryReq"}}},"required":true},"responses":{"200":{"description":"Rows"}},"security":[{"bearer_pat":["tables:read"]},{"oauth2":["tables:read"]}]}},"/v1/api/tables/{id}/rows/{row_id}":{"delete":{"tags":["tables"],"summary":"Delete a row","operationId":"delete_row","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"row_id","in":"path","description":"Row id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Row not found"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]},"patch":{"tags":["tables"],"summary":"Update a row","operationId":"update_row","parameters":[{"name":"id","in":"path","description":"Table id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"row_id","in":"path","description":"Row id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRowReq"}}},"required":true},"responses":{"200":{"description":"Updated"},"404":{"description":"Row not found"}},"security":[{"bearer_pat":["tables:write"]},{"oauth2":["tables:write"]}]}},"/v1/api/teams/channels":{"get":{"tags":["integrations"],"summary":"List Microsoft Teams channels","description":"Lists the standard Teams channels HQ can post to (each with team_id, channel_id,\nand name), sourced from the workspace corpus catalog the Teams crawl populates,\nfor use in pickers such as the schedule editor. The response includes a connected\nflag that is false when the workspace has no active Teams installation, in which\ncase the channel list is empty. Scoped to the caller's workspace; requires the\nagents:read scope.","operationId":"list_teams_channels","responses":{"200":{"description":"Standard Teams channels HQ can post to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamsChannelsResp"}}}}},"security":[{"bearer_pat":["agents:read"]},{"oauth2":["agents:read"]}]}},"/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/UsersListResp"}}}},"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/help/articles":{"get":{"tags":["help"],"summary":"List help articles","description":"`GET /v1/help/articles` - every article's metadata (slug, title, category,\norder, summary), for the Help & Docs browse view. Global corpus.","operationId":"help_articles","responses":{"200":{"description":"Help article metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticlesResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]},{"cookie_session":[]}]},"post":{"tags":["help"],"summary":"Create a help article (operator)","description":"`POST /v1/help/articles` - create a new article. Operator-only. The new row\nis chunked + embedded + indexed before this returns.","operationId":"help_create_article","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateHelpReq"}}},"required":true},"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}},"403":{"description":"Operator access required"},"409":{"description":"Slug already exists"}},"security":[{"cookie_session":[]}]}},"/v1/help/articles/{slug}":{"get":{"tags":["help"],"summary":"Get a help article","description":"`GET /v1/help/articles/{slug}` - one article's full markdown body.","operationId":"help_article","parameters":[{"name":"slug","in":"path","description":"Article slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The article","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}},"404":{"description":"No such article"}},"security":[{"bearer_pat":[]},{"oauth2":[]},{"cookie_session":[]}]},"put":{"tags":["help"],"summary":"Update a help article (operator)","description":"`PUT /v1/help/articles/{slug}` - replace an article's fields. Operator-only.\nRe-chunked + re-embedded + re-indexed before returning.","operationId":"help_update_article","parameters":[{"name":"slug","in":"path","description":"Article slug","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertHelpReq"}}},"required":true},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}},"403":{"description":"Operator access required"},"404":{"description":"No such article"}},"security":[{"cookie_session":[]}]},"delete":{"tags":["help"],"summary":"Delete a help article (operator)","description":"`DELETE /v1/help/articles/{slug}` - remove an article. Operator-only. Its\nchunks cascade-delete and the Torque index is reconciled before returning.","operationId":"help_delete_article","parameters":[{"name":"slug","in":"path","description":"Article slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{}}}},"403":{"description":"Operator access required"}},"security":[{"cookie_session":[]}]}},"/v1/help/search":{"get":{"tags":["help"],"summary":"Search the help corpus","description":"`GET /v1/help/search?q=...` - hybrid search: Torque BM25 (lexical) UNION\npgvector ANN (dense), fused + ordered by a cross-encoder reranker, best\nchunk per article. The same recall stack workspace search uses. Degrades to\ndense-only, lexical-only, or a Postgres FTS fallback if a backend is down.","operationId":"help_search","parameters":[{"name":"q","in":"query","description":"The search query.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ranked help results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResp"}}}}},"security":[{"bearer_pat":[]},{"oauth2":[]},{"cookie_session":[]}]}},"/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/McpCatalogResp"}}}}},"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/catalog/{slug}/tools":{"get":{"tags":["integrations"],"summary":"List an MCP server's tools","description":"Returns the captured tool list (name + one-line description) for one catalog\nserver, scoped to the caller's workspace for custom (`t:`) backends. Empty\nuntil the backend's schema has been captured. Admin only.","operationId":"mcp_catalog_tools","parameters":[{"name":"slug","in":"path","description":"MCP server slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Captured tool list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogToolsResp"}}}},"404":{"description":"No such server"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/connectors":{"get":{"tags":["integrations"],"summary":"List connectors","description":"Returns every connector registered in the caller's workspace, newest first, with each\nconnector's id, name, status, last-seen timestamp, and creation time. Admin only;\nscoped to the caller's own workspace.","operationId":"connector_list","responses":{"200":{"description":"Connectors in this workspace","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorRow"}}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"post":{"tags":["integrations"],"summary":"Create a connector","description":"Registers a new connector for the caller's workspace and returns a one-time\nenrollment token (shown only here, never stored) along with the tunnel address and\nserver name the connector agent dials out to. Admin only; scoped to the caller's own\nworkspace.","operationId":"connector_create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCreateReq"}}},"required":true},"responses":{"200":{"description":"The created connector + its one-time enrollment token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCreateResp"}}}},"400":{"description":"Empty name"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/connectors/enroll":{"post":{"tags":["integrations"],"summary":"Enroll a connector","description":"Completes connector enrollment: the connector agent presents its one-time enrollment\ntoken and a PEM-encoded certificate signing request, and receives a CA-signed client\ncertificate, the CA certificate, and the tunnel address it should dial. This endpoint\nis public and authenticated only by the one-time token (the agent has no session); the\ntoken is single-use and time-limited. Returns 403 when the token is missing, invalid,\nexpired, or already redeemed.","operationId":"connector_enroll","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorEnrollReq"}}},"required":true},"responses":{"200":{"description":"The signed client cert + CA + tunnel address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorEnrollResp"}}}},"403":{"description":"Invalid, expired, or already-used enrollment token"}}}},"/v1/mcp/connectors/{id}":{"delete":{"tags":["integrations"],"summary":"Revoke a connector","description":"Revokes the given connector in the caller's workspace: its certificate fingerprint\nand any pending enrollment token are cleared so no new mTLS sessions can be\nestablished, and live sessions drop on their next reconnect. Admin only; scoped to the\ncaller's own workspace. Returns 204 No Content.","operationId":"connector_revoke","parameters":[{"name":"id","in":"path","description":"Connector id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The connector was revoked"},"404":{"description":"No such connector in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/connectors/{id}/mcp":{"post":{"tags":["integrations"],"summary":"Register a private MCP behind a connector","description":"Registers a private MCP server reached through the given connector and installs it\n(enabled) for the caller's workspace. The MCP is exposed at the connector's local base\npath (defaulting to `/mcp`) and may carry an optional per-day call cap. Returns the\ngenerated catalog slug for the new integration. Admin only; scoped to the caller's own\nworkspace.","operationId":"connector_add_mcp","parameters":[{"name":"id","in":"path","description":"Connector id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorAddMcpReq"}}},"required":true},"responses":{"200":{"description":"The registered MCP's catalog slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorAddMcpResp"}}}},"400":{"description":"Empty display_name or the connector is revoked"},"404":{"description":"No such connector in this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom":{"post":{"tags":["integrations"],"summary":"Add a custom MCP","description":"Provisions a private custom MCP integration for the caller's workspace from a pasted\nserver URL. The server is re-probed and must be self-serve connectable; the workspace\ncustom-integration policy is enforced first. A private catalog entry is created and,\nfor open (no-auth) servers, enabled immediately (or held for review when the policy\nrequires approval); OAuth and API-key servers return the next step the caller must\ntake to finish connecting. Returns the new slug, display name, credential model, and\nthat next step. Admin only; scoped to the caller's own workspace.","operationId":"mcp_custom_create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomCreateReq"}}},"required":true},"responses":{"200":{"description":"The provisioned custom integration + next step","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomCreateResp"}}}},"400":{"description":"Not connectable, policy disallows it, or discovery failed"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom/policy":{"get":{"tags":["integrations"],"summary":"Get the custom-MCP policy","description":"Returns the caller's workspace policy for self-serve custom MCP integrations: whether\ncustom integrations are enabled, the allow-list and block-list of domains, and whether\nnewly added integrations require admin approval before agents can use them. Admin only;\nscoped to the caller's own workspace.","operationId":"mcp_custom_policy_get","responses":{"200":{"description":"The workspace's custom-MCP policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomPolicyResp"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]},"put":{"tags":["integrations"],"summary":"Update the custom-MCP policy","description":"Replaces the caller's workspace policy for self-serve custom MCP integrations: toggles\nwhether custom integrations are allowed at all, sets the allow-list and block-list of\ndomains (each normalized to a bare host), and sets whether newly added integrations\nmust be approved by an admin before agents can use them. Returns the stored policy.\nAdmin only; scoped to the caller's own workspace.","operationId":"mcp_custom_policy_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomPolicyReq"}}},"required":true},"responses":{"200":{"description":"The stored custom-MCP policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomPolicyResp"}}}}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom/probe":{"post":{"tags":["integrations"],"summary":"Probe a custom MCP URL","description":"Dry-run discovery for a pasted MCP server URL: detects the server's authentication\nmethod (none, API key, or OAuth), reports whether the workspace can finish connecting\nit self-serve, and previews the server's tool list - without writing anything. The URL\nis checked against the workspace's custom-integration policy before any outbound call\nis made. Admin only; scoped to the caller's own workspace.","operationId":"mcp_custom_probe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomProbeReq"}}},"required":true},"responses":{"200":{"description":"Detected auth + connectability + tool preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomProbeResp"}}}},"400":{"description":"Invalid/blocked URL, policy disallows it, or discovery failed"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom/{slug}":{"delete":{"tags":["integrations"],"summary":"Remove a custom MCP","description":"Deletes a custom integration the caller's workspace previously added, along with its\ninstall rows, stored credentials, and per-agent enablement. Only integrations owned by\nthe caller's own workspace (slug prefix `t:`) can be removed here; vetted\nplatform/third-party integrations cannot. Admin only. Returns 204 No Content.","operationId":"mcp_custom_delete","parameters":[{"name":"slug","in":"path","description":"Custom integration slug (t:<tenant>:<n>)","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"The custom integration was removed"},"400":{"description":"Not a custom integration owned by this workspace"},"404":{"description":"No such integration"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom/{slug}/approve":{"post":{"tags":["integrations"],"summary":"Approve a custom MCP","description":"Approves a custom integration that is held for review (either on first add under an\napproval-required policy, or after its tool set changed since the last approval). The\ncurrently-captured tool set is recorded as the approved baseline and the integration\nis re-activated so agents can use it again. Only integrations owned by the caller's own\nworkspace can be approved. Admin only. Returns 204 No Content.","operationId":"mcp_custom_approve","parameters":[{"name":"slug","in":"path","description":"Custom integration slug (t:<tenant>:<n>)","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"The custom integration was approved + re-activated"},"400":{"description":"Not a custom integration owned by this workspace"},"404":{"description":"No install to approve for that slug"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/custom/{slug}/limits":{"put":{"tags":["integrations"],"summary":"Set a custom MCP's daily call cap","description":"Sets or clears the per-day call cap for one of the caller's workspace's custom\nintegrations; a null or zero value removes the cap (uncapped). Only integrations owned\nby the caller's own workspace can be limited here. Admin only. Returns 204 No Content.","operationId":"mcp_custom_limits","parameters":[{"name":"slug","in":"path","description":"Custom integration slug (t:<tenant>:<n>)","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomLimitsReq"}}},"required":true},"responses":{"204":{"description":"The daily call cap was updated"},"400":{"description":"Not a custom integration owned by this workspace"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/directory/search":{"get":{"tags":["integrations"],"summary":"Search the public MCP ecosystem (Discover)","description":"Read-only search over the wild MCP directory - public servers that exist in\nthe open ecosystem, NOT the integrations this workspace runs. Each hit notes\nwhether it is hostable (addable as a custom integration). Admin only; 503s\nwhen the directory database is not configured.","operationId":"mcp_directory_search","parameters":[{"name":"q","in":"query","description":"Free-text query, min 2 chars","required":true,"schema":{"type":"string"}},{"name":"category","in":"query","description":"Optional exact category filter","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Max results (default 20, max 50)","required":false,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Directory hits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectorySearchResp"}}}},"503":{"description":"Directory database not configured"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/v1/mcp/directory/stats":{"get":{"tags":["integrations"],"summary":"Public MCP directory size (Discover landing)","description":"Read-only counts over the wild MCP directory for the Discover empty-state.\nAdmin only; 503s when the directory database is not configured.","operationId":"mcp_directory_stats","responses":{"200":{"description":"Directory counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryStats"}}}},"503":{"description":"Directory database not configured"}},"security":[{"bearer_pat":["admin"]},{"oauth2":["admin"]}]}},"/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/McpInstallReq"}}},"required":true},"responses":{"200":{"description":"The installed / re-enabled server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpInstallResp"}}}},"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/McpInstallResp"}}}}},"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/device_authorization":{"post":{"tags":["auth"],"summary":"OAuth 2.0 Device Authorization Request (RFC 8628 §3.1-3.2)","description":"Starts a device-code flow with PKCE. Returns a `device_code` the client polls\n`/v1/oauth/token` with (grant_type=urn:ietf:params:oauth:grant-type:device_code)\nand a short `user_code` the user approves at `verification_uri`. No HQ session\nis required here - the user authenticates later, on the verification page.","operationId":"oauth_device_authorization","requestBody":{"description":"Device authorization request. application/json or application/x-www-form-urlencoded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthzReq"}}},"required":true},"responses":{"200":{"description":"device_code + user_code + verification URIs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthzResp"}}}},"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/SkillsCatalogResp"}}}}},"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/SkillsInstallReq"}}},"required":true},"responses":{"200":{"description":"Skill installed / re-enabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillsInstallResp"}}}},"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/SkillsInstallResp"}}}},"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/SkillUploadReq"}}},"required":true},"responses":{"201":{"description":"Workspace skill created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillUploadResp"}}}},"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"},"review_owner_id":{"type":["string","null"],"format":"uuid","description":"The HQ user this `review_reply` item is attributed to; `null` = the\nadmin Unassigned bucket. Only meaningful on the review_reply queue."},"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"]}}},"AgentRef":{"type":"object","required":["slug","display_name"],"properties":{"display_name":{"type":"string"},"slug":{"type":"string"}}},"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."}}},"AgentsListResp":{"type":"object","required":["agents"],"properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/AgentRow"}}}},"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}}},"Article":{"type":"object","required":["slug","title","category","ord","summary","body"],"properties":{"body":{"type":"string"},"category":{"type":"string"},"ord":{"type":"integer","format":"int32"},"slug":{"type":"string"},"summary":{"type":"string"},"title":{"type":"string"}}},"ArticleMeta":{"type":"object","required":["slug","title","category","ord","summary"],"properties":{"category":{"type":"string"},"ord":{"type":"integer","format":"int32"},"slug":{"type":"string"},"summary":{"type":"string"},"title":{"type":"string"}}},"ArticlesResp":{"type":"object","required":["articles"],"properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/ArticleMeta"}}}},"ArtifactLite":{"type":"object","required":["id","name","content_type","size_bytes","sensitive","created_at","download_url","public_url"],"properties":{"content_type":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"delivered_seq":{"type":["integer","null"],"format":"int64","description":"Per-conversation ordering key (migration 0238): the journal `seq` at which\nthis artifact was delivered. Clients interleave artifacts into the transcript\nby `seq` (the same key message bubbles carry), so a mid-turn chart lands in\nthe same place live and on reload. `null` for rows published before 0238 -\nfall back to `created_at` ordering then."},"download_url":{"type":"string"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"public_url":{"type":"string","description":"Absolute, non-expiring PUBLIC capability URL (no auth). The web uses the\nauth `download_url` (it has the session); a native client embeds artifacts\nin a WebView that can't carry auth, so it loads images from this."},"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."}}},"AssignOwnerReq":{"type":"object","required":["user_id"],"properties":{"user_id":{"type":"string","format":"uuid","description":"HQ user to attribute this review-reply item to (it moves into their\nreview queue). Must be a workspace member."}}},"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. Three 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  * **Artifact reference** (`artifact_id`) - point the turn at an\n    existing agent-published `artifacts` row (a chart, a generated\n    PDF, a prior deliverable). Resolved WORKSPACE-scoped, not\n    conversation-scoped: a scheduled fire or a \"reference an\n    existing file\" pick legitimately points at an artifact created\n    in another conversation of the same tenant. Loaded from the\n    `hq-artifacts` bucket and staged into `/work/uploads/` exactly\n    like a document. This is the durable, id-addressed handle a\n    schedule stores (a `/work` path would not survive the next fire).\n\n`filename` is optional on the `document_id` / `artifact_id` paths\n(the row already carries one); inline callers must supply it.\n`content_type` follows the same rule.","properties":{"artifact_id":{"type":["string","null"],"format":"uuid","description":"Reference to an existing `artifacts` row in the caller's tenant\n(workspace-scoped). The turn driver loads bytes from the\n`hq-artifacts` bucket server-side."},"content_type":{"type":["string","null"]},"data_b64":{"type":["string","null"],"description":"Inline base64 bytes. Provide exactly one of this / `document_id`\n/ `artifact_id`."},"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"]}}},"AuditListResp":{"type":"object","required":["events","total","page","page_size"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/AuditRow"}},"page":{"type":"integer","format":"int64"},"page_size":{"type":"integer","format":"int64"},"total":{"type":"integer","format":"int64"}}},"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)."}}},"AvailResp":{"type":"object","required":["available"],"properties":{"available":{"type":"boolean"},"reason":{"type":["string","null"],"description":"Why not, when unavailable: `taken` | `reserved` | `format` | `too_short`."},"suggestion":{"type":["string","null"],"description":"A free alternative the UI can offer (e.g. `hq-2`), when `taken`."}}},"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","review_reply"],"properties":{"awaiting_review":{"type":"integer","format":"int64"},"count":{"type":"integer","format":"int64","description":"Sum of the queues the caller can see (the nav-badge number)."},"quarantined":{"type":"integer","format":"int64"},"review_reply":{"type":"integer","format":"int64"},"unrouted":{"type":"integer","format":"int64"}}},"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."},"max_calls_per_day":{"type":["integer","null"],"format":"int64","description":"Current per-day call cap parsed from `budget_defaults`; `None` =\nuncapped. Drives the editable daily-limit affordance on custom\n(`t:`) integration cards."},"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"}}},"CatalogToolInfo":{"type":"object","required":["name"],"properties":{"description":{"type":["string","null"]},"name":{"type":"string"}}},"CatalogToolsResp":{"type":"object","required":["tools"],"properties":{"tools":{"type":"array","items":{"$ref":"#/components/schemas/CatalogToolInfo"},"description":"May be empty until the backend first connects and its schema is captured."}}},"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"]}}},"CollabTicketResp":{"type":"object","required":["ticket","ws_path","expires_in_secs","capability"],"properties":{"capability":{"type":"string","description":"Capability the ticket grants: `edit` | `read`."},"expires_in_secs":{"type":"integer","format":"int64","description":"Seconds until the ticket expires."},"ticket":{"type":"string","description":"The signed, single-use ticket. Open the websocket with it as a\n`tkt.<ticket>` entry in `Sec-WebSocket-Protocol` (alongside the\n`hq-collab.v1` subprotocol) - never in the URL."},"ws_path":{"type":"string","description":"Same-origin websocket path; the client composes `wss://<host>` + this."}}},"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":{},"court_intake_enabled":{"type":["boolean","null"],"description":"Court-document intake on/off (docs/court-document-intake.md). Omitted =\nkeep current."},"court_secure_mail_password":{"type":["string","null"],"description":"Secure-mail password for the court portal. WRITE-ONLY: a non-empty value\nis vault-sealed and stored; it is never echoed back. Omitted/empty =\nkeep the stored password."},"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"}}},"ConnectorAddMcpReq":{"type":"object","required":["display_name"],"properties":{"display_name":{"type":"string"},"max_calls_per_day":{"type":["integer","null"],"format":"int64","description":"Optional per-day call cap (P2)."},"path":{"type":["string","null"],"description":"Path of the private MCP on the connector's local base (default\n`/mcp`)."}}},"ConnectorAddMcpResp":{"type":"object","required":["slug"],"properties":{"slug":{"type":"string"}}},"ConnectorCreateReq":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"ConnectorCreateResp":{"type":"object","required":["connector_id","enrollment_token","tunnel_addr","tunnel_server_name"],"properties":{"connector_id":{"type":"string","format":"uuid"},"enrollment_token":{"type":"string","description":"Shown once - the admin runs the agent with it. Not stored."},"tunnel_addr":{"type":"string"},"tunnel_server_name":{"type":"string"}}},"ConnectorEnrollReq":{"type":"object","required":["token","csr_pem"],"properties":{"csr_pem":{"type":"string"},"token":{"type":"string"}}},"ConnectorEnrollResp":{"type":"object","required":["connector_id","cert_pem","ca_pem","tunnel_addr","tunnel_server_name"],"properties":{"ca_pem":{"type":"string"},"cert_pem":{"type":"string"},"connector_id":{"type":"string","format":"uuid"},"tunnel_addr":{"type":"string"},"tunnel_server_name":{"type":"string"}}},"ConnectorRow":{"type":"object","required":["id","name","status","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_seen_at":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"status":{"type":"string"}}},"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","usage"],"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."},"usage":{"$ref":"#/components/schemas/ConvUsage","description":"Token + cost this conversation has consumed (migration 0298)."}}},"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","usage"],"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"},"document_id":{"type":["string","null"],"format":"uuid","description":"The live document this conversation is bound to (the editor's Document\ncolumn), or null. When set, Studio opens the Scriptor \"Document\" column\nfor it - the durable signal behind the `?doc=` deep-link."},"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\"."},"usage":{"$ref":"#/components/schemas/ConvUsage","description":"Token + cost this conversation has consumed (migration 0298). Studio\nrenders it as a small usage line."},"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"}}}},"ConvUsage":{"type":"object","description":"Per-conversation token + cost rollup (migration 0298). Zeroed until the\nconversation has run a turn. `cost_usd` is the charged (credit) price.","required":["turns","new_tokens","cache_tokens","total_tokens","cost_usd"],"properties":{"cache_tokens":{"type":"integer","format":"int64","description":"Prompt-cache tokens: cache read + write (usually the bulk)."},"cost_usd":{"type":"number","format":"double"},"new_tokens":{"type":"integer","format":"int64","description":"Non-cache tokens: input + output (the genuinely new work)."},"total_tokens":{"type":"integer","format":"int64","description":"new_tokens + cache_tokens."},"turns":{"type":"integer","format":"int64"}}},"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"}}},"CreateAgentReq":{"type":"object","required":["slug","display_name"],"properties":{"aliases":{"type":"array","items":{"type":"string"}},"avatar_url":{"type":["string","null"],"description":"Optional avatar URL - absolute https URL only. Use the\n/v1/agents/{id}/avatar upload endpoint to host a file in\nTrove and get back a URL to put here."},"cautions":{"type":"array","items":{"type":"string"}},"description":{"type":["string","null"]},"display_name":{"type":"string"},"enabled_mcp_slugs":{"type":["array","null"],"items":{"type":"string"}},"enabled_skill_slugs":{"type":["array","null"],"items":{"type":"string"},"description":"Initial enabled subsets. Pass `None` (default) for \"all\ninstalled\" - the empty-row convention in agent_skills_enabled\n/ agent_mcp_enabled means \"all the tenant has installed\"."},"instructions":{"type":["string","null"],"description":"Per-agent system-prompt instructions - what the agent does,\nscope, tone. NULL/empty = platform-baseline behavior only."},"languages":{"type":"array","items":{"type":"string"}},"model":{"type":["string","null"],"description":"Optional model slug for this agent (must belong to runtime_profile).\nOmitted/empty = the runtime's platform default. Billing C."},"runtime_profile":{"type":"string"},"slug":{"type":"string"},"tone":{"type":["string","null"],"description":"Personality knobs - structured-rendering into the system\nprompt. See `agents_api::validate_tone` / `validate_verbosity`\n/ `normalise_cautions` / `normalise_languages` for the rules."},"verbosity":{"type":["string","null"]}}},"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"}}},"CreateHelpReq":{"type":"object","required":["slug","title","category","body"],"properties":{"body":{"type":"string"},"category":{"type":"string"},"ord":{"type":"integer","format":"int32"},"slug":{"type":"string"},"summary":{"type":"string"},"title":{"type":"string"}}},"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"]},"max_duration_secs":{"type":["integer","null"],"format":"int32","description":"Wall-clock cap per fire, in seconds (default 1800, clamped 60..=3600). A\nfire that runs past it is killed and marked failed; size it for the\nschedule's step budget."},"max_steps":{"type":["integer","null"],"format":"int32","description":"Per-turn step budget (max tool-call rounds) for each fire. Omit for the\ndefault of 200 - the same ceiling an interactive turn gets. Raise for\nheavy multi-step schedules; clamped 1..=1000 at fire time.","minimum":0},"name":{"type":"string"},"prompt":{"type":"string"},"referenced_artifact_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Durable file references staged into `/work/uploads/` before each fire.\nIds only (artifacts / documents / documents-folders); a `/work` path in\n`prompt` is rejected (the dir is recreated empty each run). Folder ids\nre-resolve to their current members at every fire."},"referenced_document_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"referenced_folder_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"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."}}},"CreateWorkspaceReq":{"type":"object","required":["workspace_name","invite_code"],"properties":{"invite_code":{"type":"string","description":"Invite code (`inv-…`). Required and consumed single-use - creating a\nworkspace is invite-gated even for an already-authenticated member, so a\nsigned-in user can't mint workspaces without an invite to spend."},"website":{"type":["string","null"],"description":"Optional company website / domain; seeds the brand-style crawl. Falls back\nto the caller's email domain (unless free-mail) when omitted."},"workspace_name":{"type":"string","description":"Display name for the new workspace; the URL slug derives from it."}}},"CreateWorkspaceResp":{"type":"object","required":["slug","name","onboarded"],"properties":{"name":{"type":"string","description":"Display name."},"onboarded":{"type":"boolean","description":"`false` for a fresh create (the SPA lands the owner on `/onboarding`); on\nan idempotent replay it reflects the tenant's real onboarding state."},"slug":{"type":"string","description":"Slug of the freshly created workspace."}}},"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"}}}},"CustomCreateReq":{"type":"object","required":["url"],"properties":{"display_name":{"type":["string","null"],"description":"Optional label; defaults to the server's self-reported name,\nelse its host."},"max_calls_per_day":{"type":["integer","null"],"format":"int64","description":"Optional per-day call cap for the agent's use of this\nintegration (P2). `None`/0 = uncapped."},"url":{"type":"string"}}},"CustomCreateResp":{"type":"object","required":["slug","display_name","credential_model","next"],"properties":{"credential_model":{"type":"string"},"display_name":{"type":"string"},"next":{"type":"string","description":"What the UI should do next: `connected` | `oauth_start` |\n`api_key`."},"slug":{"type":"string"}}},"CustomLimitsReq":{"type":"object","properties":{"max_calls_per_day":{"type":["integer","null"],"format":"int64","description":"`None`/0 = remove the cap (uncapped)."}}},"CustomPolicyResp":{"type":"object","required":["custom_mcp_enabled","allowed_domains","blocked_domains","require_approval"],"properties":{"allowed_domains":{"type":"array","items":{"type":"string"}},"blocked_domains":{"type":"array","items":{"type":"string"}},"custom_mcp_enabled":{"type":"boolean"},"require_approval":{"type":"boolean"}}},"CustomProbeReq":{"type":"object","description":"Enqueue a `hq.mcp.tenant.{tenant_id}.changed` event into the\noutbox. The publisher relays to NATS; every gateway instance\nreceives it and invalidates the per-tenant Forge catalog cache.","required":["url"],"properties":{"url":{"type":"string"}}},"CustomProbeResp":{"type":"object","required":["endpoint","auth","auth_label","connectable","tool_count","tools"],"properties":{"auth":{"type":"string","description":"`none` | `api_key` | `oauth` | `oauth_unsupported`"},"auth_label":{"type":"string","description":"Short human label for the detected method."},"connectable":{"type":"boolean","description":"True when the tenant can finish the connection with no operator\ninvolvement (open, OAuth + DCR public client, or paste-a-key)."},"endpoint":{"type":"string"},"note":{"type":["string","null"],"description":"Set when `connectable` is false: why, in one sentence."},"server_name":{"type":["string","null"]},"tool_count":{"type":"integer","minimum":0},"tools":{"type":"array","items":{"$ref":"#/components/schemas/CustomToolPreview"}}}},"CustomToolPreview":{"type":"object","required":["name"],"properties":{"description":{"type":["string","null"]},"name":{"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"}}},"DeliverArtifactReq":{"type":"object","required":["target"],"properties":{"target":{"$ref":"#/components/schemas/DeliverTarget"}}},"DeliverArtifactResp":{"type":"object","required":["provider","channel_id","permalink"],"properties":{"channel_id":{"type":"string","description":"The channel the file landed in."},"permalink":{"type":"string","description":"Slack permalink to the uploaded file (unfurls into the file preview)."},"provider":{"type":"string","description":"Delivery provider that handled it (`slack`)."},"ts":{"type":["string","null"],"description":"`ts` of the `chat.postMessage` that delivered the file, when Slack\nreturned one."}}},"DeliverTarget":{"oneOf":[{"type":"object","description":"Post the artifact into a Slack channel the HQ bot is a member of. The\n`channel_id` is a Slack `C…`/`G…` id from `GET /v1/api/slack/channels`.","required":["channel_id","kind"],"properties":{"channel_id":{"type":"string"},"kind":{"type":"string","enum":["slack_channel"]}}}],"description":"Where to deliver an artifact. A tagged union so the same endpoint can grow a\n`teams_channel` arm later without a breaking change (the channel picker + the\n`DeliveredRef`-shaped response already generalize)."},"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"}}},"DeviceAuthzReq":{"type":"object","required":["client_id","code_challenge"],"properties":{"client_id":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":["string","null"]},"scope":{"type":["string","null"]}}},"DeviceAuthzResp":{"type":"object","required":["device_code","user_code","verification_uri","verification_uri_complete","expires_in","interval"],"properties":{"device_code":{"type":"string"},"expires_in":{"type":"integer","format":"int64"},"interval":{"type":"integer","format":"int64"},"user_code":{"type":"string"},"verification_uri":{"type":"string"},"verification_uri_complete":{"type":"string"}}},"DeviceTokenOkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"DirectoryHit":{"type":"object","description":"One wild-directory hit. NOT an installed integration - a discovery\nhint that may require admin setup and is not guaranteed to work.","required":["key","categories","tools","remote_endpoints","hostable","addability"],"properties":{"addability":{"type":"string","description":"Short human hint on whether / how this might be added."},"authority_tier":{"type":["string","null"],"description":"`S` first-party/vendor, `A` trusted OSS, `B`/`C` community."},"categories":{"type":"array","items":{"type":"string"}},"description":{"type":["string","null"]},"domain":{"type":["string","null"],"description":"Host of the server's best URL (remote > homepage > repo)."},"github_stars":{"type":["integer","null"],"format":"int32"},"homepage":{"type":["string","null"]},"hostable":{"type":"boolean","description":"True when a hosted remote endpoint is advertised - the only kind\nthat can even be attempted as a paste-a-URL custom integration."},"key":{"type":"string","description":"Stable id (the directory's canonical key) for list rendering."},"liveness":{"type":["string","null"],"description":"Last live-handshake status (`ok`, `ok-tools`, `auth-required`,\n`dead`), or null/`requires-sandbox` for un-probed local servers."},"name":{"type":["string","null"]},"remote_endpoints":{"type":"array","items":{"$ref":"#/components/schemas/DirectoryRemote"}},"repo_url":{"type":["string","null"]},"tools":{"type":"array","items":{"type":"string"},"description":"Captured tool names (what it can do), when the server was probed."}}},"DirectoryRemote":{"type":"object","properties":{"transport":{"type":["string","null"]},"url":{"type":["string","null"]}}},"DirectorySearchResp":{"type":"object","required":["query","note","results"],"properties":{"note":{"type":"string","description":"Caveat to relay to the admin: discovered, not installed."},"query":{"type":"string"},"results":{"type":"array","items":{"$ref":"#/components/schemas/DirectoryHit"}}}},"DirectoryStats":{"type":"object","required":["total","hostable","live"],"properties":{"hostable":{"type":"integer","format":"int64","description":"Those advertising a hosted remote endpoint (addable as a custom\nintegration)."},"live":{"type":"integer","format":"int64","description":"Hosted servers whose endpoint responded to a handshake (live)."},"total":{"type":"integer","format":"int64","description":"All present servers in the directory."}}},"DocAssistantReq":{"type":"object","required":["agent_id"],"properties":{"agent_id":{"type":"string","format":"uuid"},"as_copy":{"type":"boolean","description":"Open an editable COPY of the document instead of the document itself.\nFind-or-creates the caller's working copy (shares the source's immutable\nblob, gets its own clean live state + `source_document_id` provenance) so\nthe source stays a pristine read-only original. One copy per\n(owner, source), so repeated opens reuse it. Default false (edit in place)."}}},"DocAssistantResp":{"type":"object","properties":{"conversation_id":{"type":["string","null"],"format":"uuid","description":"The conversation to load + continue (POST always returns one; GET returns\n`null` when the caller has no thread yet for this (document, agent))."},"document_id":{"type":["string","null"],"format":"uuid","description":"The document the editor should open: the path `id` normally, or the\nworking-copy id when `as_copy` was set. `null` from GET."}}},"DocSelection":{"type":"object","description":"An edit-stable selection the user marked in the open document, carried on a\nsingle doc-assistant turn. `anchor` is an opaque base64 Scriptor AnchorRange\n(resolved against the live document at apply time); `quote` is the selected\ntext, shown to the agent for grounding. See docs/document-editing.md 10.4.","required":["anchor"],"properties":{"anchor":{"type":"string","description":"Opaque base64 AnchorRange. The agent passes it verbatim as the `anchor`\nargument to `document_propose_edit`; it never parses it."},"quote":{"type":"string","description":"The selected text, for the agent's context (not used to locate the span)."}}},"DocumentPatchReq":{"type":"object","properties":{"caption":{"type":["string","null"],"description":"`null` clears the caption, omitted keeps it."},"category":{"type":["string","null"],"description":"`null` clears the category, omitted keeps it."},"channel_id":{"type":["string","null"],"description":"`null` clears the channel, omitted keeps it."},"folder_id":{"type":["string","null"],"description":"Move into a folder. `null` moves to the root of the document's scope;\nomitted keeps it. The target folder must be visible to the caller and\nshare the document's (post-patch) scope - cross-scope placement is a\n`scope` change (a share), set both together."},"scope":{"type":["string","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}}}},"DocumentRow":{"type":"object","required":["id","owner_user_id","scope","filename","content_type","size_bytes","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"},"folder_id":{"type":["string","null"],"format":"uuid","description":"Folder this document lives in. NULL = the root of its scope."},"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","null"],"description":"NULL for born-digital Live Documents (no Trove origin - content is the op-log)."},"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"}}},"DocumentUploadReq":{"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"}}}},"DocumentUploadResp":{"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}}},"DocumentsListResp":{"type":"object","required":["documents","categories","tags"],"properties":{"categories":{"type":"array","items":{"type":"string"},"description":"Distinct categories across the caller's visible set - drives\nthe category-filter sidebar in the UI."},"documents":{"type":"array","items":{"$ref":"#/components/schemas/DocumentRow"}},"tags":{"type":"array","items":{"type":"string"},"description":"Distinct tags across the caller's visible set."}}},"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","court_intake_enabled","court_secure_mail_password_set","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":{},"court_intake_enabled":{"type":"boolean","description":"Court-document intake (docs/court-document-intake.md) is enabled on this\ninbox: an inbound carrying a `securemail.domstol.se` link additionally\ndownloads the court documents into the library. Off by default."},"court_secure_mail_password_set":{"type":"boolean","description":"A secure-mail password is stored (vault-sealed) for this inbox. The\npassword is write-only; this flag is the only thing the API discloses."},"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"}}},"FolderCreateReq":{"type":"object","required":["scope","name"],"properties":{"channel_id":{"type":["string","null"],"format":"uuid","description":"Required iff `scope='channel'`; the caller must be a member."},"name":{"type":"string"},"parent_id":{"type":["string","null"],"format":"uuid","description":"Parent folder (same scope). Omit / null for a root folder."},"scope":{"type":"string","description":"`private` | `team` | `channel`."}}},"FolderPatchReq":{"type":"object","properties":{"name":{"type":["string","null"],"description":"New name."},"parent_id":{"type":["string","null"],"description":"Reparent within the same scope. `null` moves to the root; omitted keeps\nit. Cannot create a cycle."}}},"FolderRow":{"type":"object","required":["id","scope","name","owner_user_id","is_owner","doc_count","created_at","updated_at"],"properties":{"channel_id":{"type":["string","null"],"format":"uuid"},"channel_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"doc_count":{"type":"integer","format":"int64","description":"Live documents directly in this folder."},"id":{"type":"string","format":"uuid"},"is_owner":{"type":"boolean"},"name":{"type":"string"},"owner_user_id":{"type":"string","format":"uuid"},"parent_id":{"type":["string","null"],"format":"uuid"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"ForgetResp":{"type":"object","required":["fact_id","status"],"properties":{"fact_id":{"type":"string","format":"uuid"},"status":{"type":"string"}}},"FreezableAgent":{"type":"object","required":["agent_slug","display_name","frozen"],"properties":{"agent_slug":{"type":"string"},"display_name":{"type":"string","description":"User-facing agent name (tenant-renamable). The slug is an internal id;\nthe UI must render this, not the lowercase slug."},"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"}}},"GenerateAvatarReq":{"type":"object","properties":{"prompt":{"type":["string","null"],"description":"Optional free-text look description (e.g. \"calm, approachable, soft\nblue palette\"). Seeds the SUBJECT only - it's wrapped server-side in\nthe house flat-vector illustration style + no-real-likeness /\nno-demographic-marker safety rails, never used as the raw prompt.\nOmitted = the default house style from the agent's name + description."}}},"GenerateAvatarResp":{"type":"object","required":["agent_id","avatar_url"],"properties":{"agent_id":{"type":"string","format":"uuid"},"avatar_url":{"type":"string"}}},"Identity":{"type":"object","required":["user_id","external_user_id","tenant_slug","source_kind","slack_connected","teams_connected","default_locale","is_admin","is_bot","profile_consent","time_format","time_zone","discovered_at","profile_editable"],"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."},"default_locale":{"type":"string","description":"Workspace-wide default UI language (tenants.default_locale, default 'en').\nThe SPA's locale chain is `locale ?? default_locale ?? 'en'`, so this is\nthe fallback for members with no `locale` of their own (e.g. an\noperator-created tenant with no Slack profile to derive one from)."},"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."},"profile_editable":{"type":"boolean","description":"True when this user's name / timezone / locale are self-editable: only\n`origin = 'email'` users (no Slack/Teams connector to sync those from).\nConnector users are read-only here - their profile is managed upstream,\nso a local edit would be clobbered by the next sync. Drives both the\nPATCH /v1/api/me gate and the /me UI's editable controls."},"real_name":{"type":["string","null"]},"slack_connected":{"type":"boolean","description":"True when THIS tenant has a live (non-revoked) Slack install. Gates\nSlack-channel delivery targets in the SPA (a tenant with no Slack must\nnot be offered a Slack channel/DM). Mirrors the truth the\n`/v1/api/slack/channels` `connected` flag computes from `slack_installs`."},"source_kind":{"type":"string","description":"Source connector kind - `slack` / `teams` / `manual`. This is the\ncaller's identity kind IN THIS workspace: a person can be a Slack\nidentity in one tenant and an email/manual identity in another (each\ntenant binds its own Slack instance), so this is per-session truth, not\na global \"this user uses Slack\". Gates Slack-DM delivery targets."},"source_workspace":{"type":["string","null"],"description":"Display name of the connected workspace (e.g. \"Truespar\")."},"teams_connected":{"type":"boolean","description":"True when THIS tenant has a live (non-revoked) Microsoft Teams install.\nThe Teams mirror of `slack_connected`: lets the SPA detect a tenant with\nno channel connected (neither Slack nor Teams) and nudge an admin to add\none. Computed from `teams_installs`, exactly as `connect_api`'s status\nendpoint does."},"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."}}},"InboxListResp":{"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."}}},"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":{}}},"InboxOkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"InsertRowsReq":{"type":"object","required":["rows"],"properties":{"rows":{"type":"array","items":{},"description":"Rows to insert; each a `{ \"<field_slug>\": <value> }` object."}}},"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"}}},"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"}}},"LiveActivityOkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"LogoUploadReq":{"type":"object","required":["filename","content_type","body_b64"],"properties":{"body_b64":{"type":"string"},"content_type":{"type":"string"},"filename":{"type":"string"}}},"McpCatalogResp":{"type":"object","required":["servers"],"properties":{"servers":{"type":"array","items":{"$ref":"#/components/schemas/CatalogServer"}}}},"McpInstallReq":{"type":"object","required":["slug"],"properties":{"config":{"description":"Optional per-tenant config blob (e.g. `allowed_tools`,\n`rate_limit`). Stored as-is in `tenant_mcp_installs.config`."},"credential_ref":{"type":["string","null"],"description":"Vault key reference for `tenant_api_key` / `tenant_oauth`\ncredential models - ignored for `none` / `platform`."},"slug":{"type":"string"}}},"McpInstallResp":{"type":"object","required":["slug","enabled"],"properties":{"enabled":{"type":"boolean"},"slug":{"type":"string"}}},"McpResp":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"array","items":{"type":"string"}}}},"MeAvatarReq":{"type":"object","required":["filename","content_type","body_b64"],"properties":{"body_b64":{"type":"string","description":"Base64-encoded image bytes."},"content_type":{"type":"string"},"filename":{"type":"string"}}},"MeAvatarResp":{"type":"object","required":["avatar_url"],"properties":{"avatar_url":{"type":"string","description":"The new internal avatar URL (`/v1/api/artifacts/{uuid}/download`)."}}},"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","gated","granted"],"properties":{"display_name":{"type":"string"},"family":{"type":["string","null"]},"gated":{"type":"boolean","description":"Requires per-workspace activation (migration 0301). Gated models\nrender greyed out unless `granted`."},"granted":{"type":"boolean","description":"Whether the caller's workspace has been activated for this model.\nAlways true for ungated models."},"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"}}}},"NewFieldReq":{"type":"object","required":["name","type"],"properties":{"indexed":{"type":"boolean"},"is_unique":{"type":"boolean"},"link_target":{"type":["string","null"],"format":"uuid","description":"Target table id for a `link_to_record` field."},"name":{"type":"string"},"options":{},"required":{"type":"boolean"},"type":{"type":"string","description":"Logical field type: text, long_text, number, currency, percent, date,\ndatetime, boolean, single_select, multi_select, attachment,\nlink_to_record, autonumber, json."}}},"NewTableReq":{"type":"object","required":["name","fields"],"properties":{"description":{"type":["string","null"]},"fields":{"type":"array","items":{"$ref":"#/components/schemas/NewFieldReq"}},"name":{"type":"string"},"slug":{"type":["string","null"]}}},"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"}}},"NotificationsOkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"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}}},"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":{"display_name":{"type":["string","null"],"description":"Self-edited display name. EMAIL-ORIGIN users only (connector users get\nthis from Slack/Teams); a connector user supplying it gets a 403."},"locale":{"type":["string","null"],"description":"Self-edited BCP-47 locale (e.g. `sv-SE`). Email-origin only."},"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)."},"tz":{"type":["string","null"],"description":"Self-edited IANA timezone (e.g. `Europe/Stockholm`). Email-origin only."}}},"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"}}},"RegisterDeviceReq":{"type":"object","required":["token"],"properties":{"bundle_id":{"type":["string","null"],"description":"The app's bundle id == the APNs topic, e.g. \"io.tic.HQ\"."},"environment":{"type":"string","description":"\"production\" (default) or \"sandbox\" (debug builds on the APNs sandbox)."},"platform":{"type":"string","description":"\"ios\" today; reserved for \"android\" (FCM) later."},"token":{"type":"string","description":"APNs device token, lowercase hex (as handed to the app by the OS)."}}},"RegisterLiveActivityReq":{"type":"object","required":["token"],"properties":{"environment":{"type":"string","description":"\"production\" (default) or \"sandbox\" (debug builds)."},"token":{"type":"string","description":"ActivityKit push token, lowercase hex."}}},"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"}}}},"RepliesResp":{"type":"object","required":["rows"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/ActivityRow"}}}},"ReplyReq":{"type":"object","required":["text"],"properties":{"html":{"type":["string","null"]},"text":{"type":"string"}}},"ReviewCounts":{"type":"object","required":["awaiting_review","unrouted","quarantined","review_reply"],"properties":{"awaiting_review":{"type":"integer","format":"int64"},"quarantined":{"type":"integer","format":"int64"},"review_reply":{"type":"integer","format":"int64","description":"Replies to the team / default sender awaiting human review. Scoped to\nthe caller (own attributed items); admins also see the Unassigned ones."},"unrouted":{"type":"integer","format":"int64"}}},"ReviewQueues":{"type":"object","required":["awaiting_review","unrouted","quarantined","review_reply","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."},"review_reply":{"type":"array","items":{"$ref":"#/components/schemas/ActivityRow"},"description":"Replies to the team / default sender. A user sees items attributed to\nthem; an admin sees all, incl. the Unassigned bucket (review_owner_id\nis null). Never re-injected into agent context."},"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}}},"RowQueryReq":{"type":"object","properties":{"filters":{"type":"array","items":{},"description":"Each entry: `{ \"field\": \"<slug>\", \"op\": \"eq|neq|gt|gte|lt|lte|contains|is_empty|is_not_empty\", \"value\": <any> }`."},"limit":{"type":["integer","null"],"format":"int64"},"offset":{"type":["integer","null"],"format":"int64"},"select":{"type":["array","null"],"items":{"type":"string"},"description":"Field slugs to return; omitted = all fields."},"sorts":{"type":"array","items":{},"description":"Each entry: `{ \"field\": \"<slug>\", \"desc\": <bool> }`."}}},"RunJson":{"type":"object","required":["id","status","started_at"],"properties":{"conversation_id":{"type":["string","null"],"format":"uuid","description":"The Studio thread backing this run's schedule, for a deep-link. See the\nmodule note: this is the schedule's most-recent thread, not necessarily\nthis exact fire's."},"duration_ms":{"type":["integer","null"],"format":"int64","description":"Wall-clock duration once finished (ms); NULL while still in flight."},"error":{"type":["string","null"],"description":"Failure detail when status = failed / cancelled."},"finished_at":{"type":["string","null"],"format":"date-time"},"id":{"type":"string","format":"uuid"},"owner_display_name":{"type":["string","null"]},"owner_external_id":{"type":["string","null"]},"owner_user_id":{"type":["string","null"],"format":"uuid"},"schedule_id":{"type":["string","null"],"format":"uuid","description":"The schedule this run fired from. NULL for a manually-started workflow."},"schedule_name":{"type":["string","null"]},"started_at":{"type":"string","format":"date-time"},"status":{"type":"string","description":"One of: created, running, suspended, completed, failed, cancelled."}}},"RunsListResp":{"type":"object","required":["runs","total","limit","offset"],"properties":{"limit":{"type":"integer","format":"int64"},"offset":{"type":"integer","format":"int64"},"runs":{"type":"array","items":{"$ref":"#/components/schemas/RunJson"}},"total":{"type":"integer","format":"int64","description":"Total rows matching the filter (ignoring limit/offset) - the pager's\ndenominator."}}},"RunsOkResp":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"ScheduleFireUsage":{"type":"object","description":"One fire's usage point for the trend series.","required":["conversation_id","at","total_tokens","output_tokens","cost_usd"],"properties":{"at":{"type":"string","format":"date-time"},"conversation_id":{"type":"string","format":"uuid"},"cost_usd":{"type":"number","format":"double"},"output_tokens":{"type":"integer","format":"int64"},"total_tokens":{"type":"integer","format":"int64"}}},"ScheduleJson":{"type":"object","required":["id","owner_user_id","name","trigger","args","state","fire_count","failure_count","consecutive_failure_count","max_duration_secs"],"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."},"max_duration_secs":{"type":"integer","format":"int32","description":"Wall-clock cap per fire (seconds). A fire that runs past it is killed\nand marked failed. Editable; default 1800."},"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":{}}},"ScheduleUsageResp":{"type":"object","description":"Token + cost consumption for one schedule, aggregated across its fires\n(each fire runs in its own conversation - migration 0298 stamps `schedule_id`\non those, so this is an indexed rollup, not a channel_thread scan).","required":["schedule_id","fires","turns","input_tokens","output_tokens","cache_read_tokens","cache_write_tokens","total_tokens","cost_usd","vendor_cost_usd","credits","avg_cost_per_fire_usd","series"],"properties":{"avg_cost_per_fire_usd":{"type":"number","format":"double","description":"cost_usd / fires - the typical per-run cost for projection."},"cache_read_tokens":{"type":"integer","format":"int64"},"cache_write_tokens":{"type":"integer","format":"int64"},"cost_usd":{"type":"number","format":"double","description":"Charged cost (credit price). The tenant-facing number."},"credits":{"type":"number","format":"double"},"fires":{"type":"integer","format":"int64","description":"Fires that consumed tokens (conversations with a usage row)."},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"schedule_id":{"type":"string","format":"uuid"},"series":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleFireUsage"},"description":"Most recent fires, oldest-first, for a trend sparkline (capped at 60)."},"total_tokens":{"type":"integer","format":"int64"},"turns":{"type":"integer","format":"int64"},"vendor_cost_usd":{"type":"number","format":"double","description":"Raw vendor list price, before credit repricing (operator/detail view)."}}},"SchedulesListResp":{"type":"object","required":["schedules","total","limit","offset"],"properties":{"limit":{"type":"integer","format":"int64"},"offset":{"type":"integer","format":"int64"},"schedules":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleJson"}},"total":{"type":"integer","format":"int64","description":"Total rows matching the filter (ignoring limit/offset) - the admin\npaginator's denominator. For `scope=mine` this equals the page size\ntoday (no paging on the personal view) but is still returned."}}},"SchedulesOkResp":{"type":"object","required":["ok"],"properties":{"next_fire_at":{"type":["string","null"],"format":"date-time"},"ok":{"type":"boolean"}}},"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`."}}},"SearchHit":{"type":"object","required":["conversation_id","channel_kind","seq","snippet","score","matched_at"],"properties":{"agent":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AgentRef","description":"The agent on the conversation, when present."}]},"channel_kind":{"type":"string"},"conversation_id":{"type":"string","format":"uuid","description":"Conversation the matched message belongs to."},"matched_at":{"type":"string","format":"date-time","description":"When the matched message was created."},"score":{"type":"number","format":"float","description":"Post-rerank relevance, higher is better."},"seq":{"type":"integer","format":"int64","description":"Per-conversation sequence number of the matched message - the\njump-to-message deep-link target."},"snippet":{"type":"string","description":"Highlighted excerpt around the match (`<mark>…</mark>`)."},"title":{"type":["string","null"],"description":"Conversation title (LLM-derived label); null if not yet titled."}}},"SearchResp":{"type":"object","required":["hits","took_ms"],"properties":{"hits":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` to fetch the next page; null when exhausted."},"took_ms":{"type":"integer","format":"int64","minimum":0}}},"SetSlugReq":{"type":"object","required":["slug"],"properties":{"slug":{"type":"string"}}},"SetSlugResp":{"type":"object","required":["slug","email_slug"],"properties":{"email_slug":{"type":"string","description":"The email-address handle after the rename (`<localpart>-<slug>@…`),\nkept in sync with `slug`."},"slug":{"type":"string","description":"The workspace's new URL handle (`/<slug>/`)."}}},"SettingsPatchReq":{"type":"object","properties":{"allow_guest_signin":{"type":["boolean","null"]},"channel_tips_enabled":{"type":["boolean","null"]},"consent_mode":{"type":["string","null"]},"corpus_history_days":{"type":["integer","null"],"format":"int32"},"default_locale":{"type":["string","null"],"description":"Workspace-wide default UI language (BCP-47-ish, e.g. `en` / `sv`). Absent\nleaves it unchanged. Cannot be cleared - the column is NOT NULL."},"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"]}}},"SettingsResp":{"type":"object","required":["source_id","source_kind","default_locale","profile_mode","consent_mode","corpus_history_days","signin_mode","allow_guest_signin","proactive_nudges_enabled","channel_tips_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)."},"channel_tips_enabled":{"type":"boolean","description":"Whether HQ posts RECURRING skill/integration recommendations in active\nchannels (separate from the one-time welcome). Default `false` - an\nadmin opts in. Only effective when `proactive_nudges_enabled` is on."},"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."},"default_locale":{"type":"string","description":"Workspace-wide default UI language (`tenants.default_locale`). The\nfallback for members with no locale of their own; the i18n resolver\nfalls back to English if this names a language with no UI translation."},"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":"Master switch for HQ posting in channels unprompted - in practice the\none-time join welcome. Default `true`; admins opt out here. Also gates\nthe recurring recommendations below (those need this on)."},"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"}}},"SkillUploadReq":{"allOf":[{"$ref":"#/components/schemas/UploadBody"},{"type":"object","required":["name"],"properties":{"description":{"type":["string","null"],"description":"One-line description shown on the Skills page."},"name":{"type":"string","description":"Display name (3–60 chars, alphanumeric + spaces/hyphens). Used\nto derive the slug."},"profile":{"type":["string","null"],"description":"Runtime profile this pack targets. Defaults to `claude-sdk`."},"provides":{"type":["string","null"],"description":"Capability slot this skill fills, e.g. `app-design`. When set (and\nthe skill is enabled), it supersedes the platform default that\nprovides the same capability for this workspace - the tenant's own\ninstructions replace ours. Brand tokens still flow via the\nworkspace preamble, independent of which design skill is active."}}}]},"SkillUploadResp":{"type":"object","required":["slug","version"],"properties":{"slug":{"type":"string"},"version":{"type":"string"}}},"SkillsCatalogResp":{"type":"object","required":["skills"],"properties":{"skills":{"type":"array","items":{"$ref":"#/components/schemas/CatalogSkill"}}}},"SkillsInstallReq":{"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"}}},"SkillsInstallResp":{"type":"object","required":["slug","enabled"],"properties":{"enabled":{"type":"boolean"},"slug":{"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."}]},"selection":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DocSelection","description":"A span the user marked in the open document (the editor's inline\nselect->ask). Carried straight through to the turn so the agent edits\nexactly that span via the anchored `document_propose_edit`. Only the\ndocument editor sets it; every other caller omits it."}]},"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."}}},"SwitchWorkspaceReq":{"type":"object","required":["slug"],"properties":{"slug":{"type":"string","description":"Tenant slug of the workspace to switch to."}}},"SwitchWorkspaceResp":{"type":"object","required":["slug","name","onboarded"],"properties":{"name":{"type":"string"},"onboarded":{"type":"boolean","description":"Whether the target tenant has finished onboarding. The SPA lands the\nuser on `/onboarding` when false (the router gate is the backstop)."},"session_token":{"type":["string","null"],"description":"Native clients (the iOS/mobile app, `X-Hq-Native: 1`) can't ride the\n`Set-Cookie` re-scope - they store an explicit bearer. When the switch is\nrequested natively we additionally mint a fresh native session token\n(`hqses_<sealed>`) for the target workspace, byte-identical underneath to\nthe cookie. `None` for web callers (they use the cookie)."},"slug":{"type":"string"}}},"TeamsChannelJson":{"type":"object","required":["team_id","channel_id","name","direct"],"properties":{"channel_id":{"type":"string","description":"The `19:...@thread.tacv2` channel id - the Connector conversation.id the\ndelivery path posts a new root message into."},"direct":{"type":"boolean","description":"`true` when HQ was explicitly ADDED to this channel (a private / shared\nchannel captured from the bot channel-add event); `false` when it's a\nSTANDARD channel reached automatically by the team install. The schedule\npicker ignores this; the admin Channels page groups by it."},"name":{"type":"string","description":"Channel display name (e.g. \"General\")."},"team_id":{"type":"string","description":"AAD team (group) id - the first half of the corpus composite id. Carried\nso the schedule's `DeliverTo::TeamsChannel` is fully addressable."}}},"TeamsChannelsResp":{"type":"object","required":["connected","channels"],"properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/TeamsChannelJson"}},"connected":{"type":"boolean","description":"Whether this workspace has a live (non-revoked) Teams install with a known\nConnector serviceUrl - the same condition the schedule API enforces before\npersisting a Teams delivery target. `false` means the picker should prompt\nto connect Teams rather than show an (empty) list."}}},"TeamsState":{"type":"object","required":["connected","connect_available"],"properties":{"connect_available":{"type":"boolean","description":"Whether \"Connect Teams\" (the Microsoft sign-in claim flow) is wired -\ntrue only when the controlplane has the Microsoft OIDC app configured\n(`MS_OIDC_CLIENT_ID`). The UI keeps the button disabled until then, so\nthe feature can ship inert and self-activate once the env is set."},"connected":{"type":"boolean"},"org_name":{"type":["string","null"]},"package_url":{"type":["string","null"],"description":"Download path for the HQ Teams app package (manifest zip) when the\ncontrolplane has one configured (`HQ_TEAMS_APP_PACKAGE`); `None`\notherwise, so the UI shows the \"ask your HQ contact\" fallback instead of\na broken link. Mirrors the extension `download_url` shape."}}},"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"]},"device_code":{"type":["string","null"],"description":"Device Authorization Grant (RFC 8628): the opaque device_code the client\npolls with. Carries its own PKCE `code_verifier`, same as the auth-code\ngrant."},"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"}}},"UpdateCustomPolicyReq":{"type":"object","required":["custom_mcp_enabled"],"properties":{"allowed_domains":{"type":"array","items":{"type":"string"}},"blocked_domains":{"type":"array","items":{"type":"string"}},"custom_mcp_enabled":{"type":"boolean"},"require_approval":{"type":"boolean"}}},"UpdateInput":{"type":"object","required":["trigger","args"],"properties":{"args":{"type":"object"},"description":{"type":["string","null"],"description":"`null` clears, omitted keeps current."},"max_duration_secs":{"type":["integer","null"],"format":"int32","description":"New wall-clock cap per fire, in seconds (clamped 60..=3600). Omitted\nleaves it unchanged."},"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"]}}},"UpdateRowReq":{"type":"object","required":["patch"],"properties":{"patch":{"description":"Fields to change: `{ \"<field_slug>\": <value> }`."}}},"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."}}},"UpsertHelpReq":{"type":"object","required":["title","category","body"],"properties":{"body":{"type":"string"},"category":{"type":"string"},"ord":{"type":"integer","format":"int32"},"summary":{"type":"string"},"title":{"type":"string"}}},"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"]}}},"UsersListResp":{"type":"object","required":["users","total","page","page_size"],"properties":{"page":{"type":"integer","format":"int64"},"page_size":{"type":"integer","format":"int64"},"total":{"type":"integer","format":"int64","description":"Total rows matching the (bot-excluded + search) filter, before\nLIMIT/OFFSET - drives the page control. A workspace can have 1500+ users."},"users":{"type":"array","items":{"$ref":"#/components/schemas/UserRow"}}}},"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"}}},"WorkspaceEntry":{"type":"object","description":"Get my profile\n","required":["slug","name","active"],"properties":{"active":{"type":"boolean","description":"True for the workspace the current session is scoped to."},"name":{"type":"string","description":"Workspace display name."},"slug":{"type":"string","description":"Tenant slug - the value carried on `X-Hq-Workspace`."}}},"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":{"court_intake_enabled":{"type":["boolean","null"],"description":"Court-document intake on/off for the team mailbox (the address court\nsecure-mail typically lands on). Omitted = keep current.\nSee docs/court-document-intake.md."},"court_secure_mail_password":{"type":["string","null"],"description":"Secure-mail password for the court portal. WRITE-ONLY: vault-sealed when\nnon-empty, never echoed back. Omitted/empty = keep the stored password."},"localpart":{"type":["string","null"]}}},"WorkspacesResp":{"type":"object","required":["workspaces"],"properties":{"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceEntry"}}}}},"securitySchemes":{"bearer_pat":{"type":"http","scheme":"bearer","bearerFormat":"hq_pat","description":"Personal Access Token. Send as `Authorization: Bearer hq_pat_...`."},"cookie_session":{"type":"apiKey","in":"cookie","name":"hq-session","description":"Browser session cookie set by sign-in. Web SPA only; not for PAT/OAuth integrators."},"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","tables:read":"Read your tables and their rows","tables:write":"Create tables and add, edit, or delete rows"}}}}}},"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"}]}