{ "openapi": "3.0.3", "info": { "title": "MaxGood.work Public BETA API", "version": "1.0.0-beta.1", "summary": "Public BETA API (Phase 1) for Enterprise Avatar Owners and Avatar Admins.", "description": "Machine-readable specification of the MaxGood.work Public BETA API\n(Phase 1). The full Stability & Use Contract — what is stable, how\nbreaking changes are announced, what an API key grants, how to get\nsupport — is the authoritative companion document and is hosted at\nhttps://blog.maxgood.work/api-documentation-beta.\n\nAll endpoints are authenticated with an org-scoped API key\n(`Authorization: Bearer mgak_.`) created\nfrom MaxGoodWeb's *Avatar Admin > Account* tab. Each key is bound\nto exactly one Avatar; calls authenticated with that key act only\non, and only on behalf of, that Avatar. The BETA API is intended\nfor server-to-server integrations; do not embed an API key in any\nclient-side code.\n\nThe BETA surface is published under the `/v1beta/` prefix. The\ninternal `/api/...` routes used by MaxGoodWeb and the messaging\nintegrations are NOT covered by this contract.\n", "contact": { "name": "MaxGood.work BETA API support", "url": "https://discord.gg/UMGkK4ES" }, "license": { "name": "Proprietary — MaxGood.work Stability & Use Contract", "url": "https://blog.maxgood.work/api-documentation-beta" } }, "servers": [ { "url": "https://api.maxgood.work/v1beta", "description": "Production" }, { "url": "https://stageapi.maxgood.work/v1beta", "description": "Stage" } ], "security": [ { "AvatarApiKey": [] } ], "tags": [ { "name": "Enterprise", "description": "Avatar identity, semantic retrieval over the Avatar's content,\nand tagging of Avatar members. These are the read-and-light-write\nendpoints that integrators reach for first — confirm the key is\ngood, look at what the Avatar can retrieve, and enrich member\ncontext.\n" }, { "name": "Content — titles", "description": "Titles are the unit of knowledge organisation in MaxGood.work — a\ntitle (for example \"Coaching for Scrum Masters\") groups a set of\nchunks. Chunks belong to a title, not directly to an Avatar; you\ncannot upload content without a title to put it under. These\nendpoints CRUD titles, list their documents and chunks, attach a\nfooter, and toggle a title's retrievability without deleting it.\n" }, { "name": "Content — chunks", "description": "Chunks are the atomic units of retrieval. Each chunk has its own\ntext and a usage count that tracks how often it has actually\nsurfaced in an Avatar reply. Editing a chunk re-embeds it so\nretrieval quality stays current; deleting a chunk is a\nsoft-delete that preserves the chunk-to-source-document linkage.\n" }, { "name": "Avatar reply", "description": "Get the Avatar's reply to a message on behalf of a named member\nof the Avatar. This is the same code path that powers the\nwebsite chat, Slack, Teams, Discord, and email integrations —\nreply quality and personalisation are identical to a member\ninteracting through any first-party channel.\n" } ], "components": { "securitySchemes": { "AvatarApiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "mgak_.", "description": "Org-scoped API key. The Avatar (`org_id`) is derived from the\ncredential; never include an `org_id` in the request.\n" } }, "schemas": { "SuccessEnvelope": { "type": "object", "required": [ "result", "response" ], "properties": { "result": { "type": "string", "enum": [ "success" ] }, "response": { "description": "Endpoint-specific payload (see each operation)." } } }, "FailureEnvelope": { "type": "object", "required": [ "result", "response" ], "properties": { "result": { "type": "string", "enum": [ "failure" ] }, "response": { "type": "string", "description": "Human-readable error message." } } }, "Title": { "type": "object", "properties": { "title_id": { "type": "string" }, "title_name": { "type": "string" }, "authors": { "type": "array", "items": { "type": "string" } }, "owner": { "type": "string" }, "description": { "type": "string" }, "is_enabled": { "type": "boolean" }, "count": { "type": "integer", "description": "Number of active chunks belonging to this title." } } }, "TitleList": { "type": "object", "properties": { "titles_count": { "type": "integer" }, "titles": { "type": "array", "items": { "$ref": "#/components/schemas/Title" } } } }, "Chunk": { "type": "object", "properties": { "text": { "type": "string" }, "title_id": { "type": "string" } } }, "ChunkSummary": { "type": "object", "properties": { "chunk_id": { "type": "string" }, "chunk_name": { "type": "string" }, "chunk_text": { "type": "string" }, "usage_count": { "type": "integer", "description": "How often this chunk has appeared as retrieval context in an Avatar reply." } } }, "ChunkList": { "type": "object", "properties": { "chunks": { "type": "array", "items": { "$ref": "#/components/schemas/ChunkSummary" } } } }, "Document": { "type": "object", "properties": { "id": { "type": "string" }, "org_id": { "type": "string" }, "title_id": { "type": "string" }, "s3_location": { "type": "string" }, "filename": { "type": "string" }, "filetype": { "type": "string" }, "mimetype": { "type": "string" }, "filesize": { "type": "integer", "format": "int64" }, "upload_date": { "type": "number", "format": "double" }, "active": { "type": "boolean" } } }, "DocumentList": { "type": "object", "properties": { "title_id": { "type": "string" }, "document_count": { "type": "integer" }, "documents": { "type": "array", "items": { "$ref": "#/components/schemas/Document" } } } }, "RagChunk": { "type": "object", "properties": { "text": { "type": "string" }, "score": { "type": "number" }, "chunk_id": { "type": "string" }, "title_id": { "type": "string" } } }, "Tag": { "type": "object", "required": [ "name", "description", "source" ], "properties": { "name": { "type": "string", "description": "Tag name (no `{}[]<>\"'` characters)." }, "description": { "type": "string" }, "source": { "type": "string" } } }, "AvatarReply": { "type": "object", "properties": { "response": { "type": "string", "description": "The Avatar's reply text. A reply that starts with `[ERROR]:` means the chat core declined to produce a response (e.g. the prompt exceeded the Avatar's input limit) and the text explains why." }, "interaction_id": { "type": "string", "description": "Server-minted id of the interaction row created for this call. Provided for reference only — do NOT send it back as input." }, "partner_content_ids": { "type": "array", "items": { "type": "string" } } } } }, "responses": { "Unauthorized": { "description": "Missing, malformed, or revoked API key.", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } }, "Forbidden": { "description": "Credential does not have the required scope.", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } }, "NotFound": { "description": "Target resource was not found, or is not owned by the caller's Avatar (the two are intentionally indistinguishable).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } }, "ValidationError": { "description": "Request body or parameters failed validation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } }, "PayloadTooLarge": { "description": "Request body exceeds the per-endpoint size limit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } } } }, "paths": { "/organization": { "get": { "tags": [ "Enterprise" ], "operationId": "getOrganization", "summary": "Get the caller's Avatar identity.", "description": "Returns the Avatar identity (`org_id`) bound to your API key —\nthe cheapest, simplest call on the BETA surface. A 200 means\nthe key is valid, not revoked, and bound to the Avatar you\nexpect; cache the returned `org_id` for the rest of your\nsession.\n\n**Why it exists.** The Avatar is derived from the credential\nrecord itself, never from the request body. A single\nround-trip to this endpoint is therefore both an\nauthentication check and an identity check — the API\nequivalent of a `whoami`.\n\n**Use cases.**\n- Boot-time health check before a long-running integration\n job.\n- Tag every outbound log line your integration emits with the\n connected Avatar's `org_id`.\n- Show \"Connected to *Avatar X*\" in your integration's\n settings UI so an admin reviewing a key knows exactly what\n it controls before configuring anything destructive.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "org_id": { "type": "string" }, "org_name": { "type": "string" } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/rag-search": { "post": { "tags": [ "Enterprise" ], "operationId": "ragSearch", "summary": "Semantic retrieval over the Avatar's own content.", "description": "Runs an Intention-Driven RAG (ID-RAG) lookup — the same\nretrieval pipeline that backs every Avatar reply — over the\nAvatar's own Expertise content and returns the matching\nchunks with their similarity scores.\n\n**Scope.** Retrieval is restricted to the caller's own Avatar\n(`org-chunks-`). The collection target,\n`top_k`, and `min_score` are all derived server-side from\nthe Avatar and its `organization_type` — they are **not**\ncaller-supplied. This keeps Public BETA retrieval\nsemantically aligned with what the Avatar itself does\ninternally when it builds a reply.\n\nFoundation content retrieval (the platform-wide collection\nthat licensed expert authors contribute to) is intentionally\n**not** exposed in Phase 1 — opening it requires additional\ntechnical work and a separate commercial / contractual\nframework. If you have a use case that needs Foundation\nretrieval through the API, please get in touch on the support\nDiscord.\n\n**Why it matters.** A coaching Avatar's value is overwhelmingly\na function of *what content it can retrieve* for a member's\nquestion. Retrieval quality determines whether replies feel\ninsightful, generic, or off-base. Calling this directly lets\nyou reason about that quality for your own Expertise content\nwithout paying the cost of a full chat call.\n\n**Use cases.**\n- **Content-gap analysis.** Feed your ICP's common questions\n through this endpoint; queries that consistently fall below\n your `min_score` cutoff reveal where your Avatar's\n Expertise needs more material.\n- **Pre-flight \"does my Avatar already cover topic X?\"**\n check before redirecting a member to an external resource.\n- **External search UI.** Surface your Avatar's content in\n your own intranet search box with the same ranking the\n Avatar itself sees at reply time.\n- **Quality-of-retrieval regression detection.** Run the same\n probe queries on a schedule and alert when the top hit's\n similarity score drops below a baseline — early warning of\n content drift, mis-tagging, or accidental disabling of a\n high-value title.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "content" ], "properties": { "content": { "type": "string", "description": "The query text to retrieve against." } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "chunks": { "type": "array", "items": { "type": "object", "properties": { "chunk_id": { "type": "string" }, "score": { "type": "number" }, "text": { "type": "string" } } } } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/ValidationError" } } } }, "/tags": { "post": { "tags": [ "Enterprise" ], "operationId": "addTagsToUser", "summary": "Add tags to a member of the Avatar.", "description": "Upserts one or more tags onto a member of the Avatar, keyed by\n`(org_id, user_id, tag_name)`. Sending the same tag name again\nupdates its description and source rather than duplicating it.\n\n**Why it matters.** Tags are first-class context for the\nAvatar — when a member interacts, the Avatar can see their\ntags and adjust its response emphasis, style, or\nrecommendations accordingly. Tagging a member is one of the\nlowest-effort, highest-leverage ways to make an Avatar's\nreplies feel personalised.\n\n**Use cases.**\n- **CRM sync.** Push HubSpot, Salesforce, or other CRM tags\n through so the Avatar's view of a member matches your\n account team's view.\n- **Behavioural enrichment.** Auto-tag based on external\n signals such as `completed-onboarding`,\n `scrum-master-cert`, or `industry:fintech` so the Avatar\n leans into the member's actual context.\n- **Learning-management bridge.** Sync completed-course or\n progress tags from an LMS so the Avatar references\n curriculum the member has actually finished.\n- **Lifecycle tags.** `trial-user`, `champion`, `at-risk` —\n let the Avatar know what to emphasise (or de-emphasise)\n without writing a custom prompt.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_email", "tags_to_add" ], "properties": { "user_email": { "type": "string", "format": "email" }, "tags_to_add": { "type": "array", "items": { "$ref": "#/components/schemas/Tag" } } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessEnvelope" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/ValidationError" } } } }, "/content/titles": { "get": { "tags": [ "Content — titles" ], "operationId": "listContentTitles", "summary": "List the Avatar's Expertise titles.", "description": "Lists the Avatar's **Expertise** titles — the titles owned by\nthe Avatar itself. Foundation titles (the global licensed\nauthor collection) are not included.\n\nEach entry carries the title id, name, enabled flag, and the\ncount of active chunks under it. This is the roster of what\nyour Avatar \"knows.\"\n\n**Why it matters.** A title is a coherent body of knowledge\n— for example, \"Coaching for Scrum Masters\" — and what is\n*enabled* here is what the Avatar can actually retrieve from\nwhen it replies. Auditing this list regularly is the simplest\nway to keep an Avatar's Expertise aligned with what your\ncustomers care about.\n\n**Use cases.**\n- External content-management dashboard for users who do not\n use MaxGoodWeb.\n- Quarterly Expertise audit — every title with its chunk\n count and enabled state, in a single report.\n- Sync titles outward to a source-of-truth knowledge base or\n CMS that already manages your content lifecycle.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "titles_count": { "type": "integer" }, "titles": { "type": "array", "items": { "type": "object", "properties": { "title_id": { "type": "string" }, "title_name": { "type": "string" }, "is_enabled": { "type": "boolean" }, "count": { "type": "integer", "description": "Number of active chunks belonging to this title." } } } } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } }, "post": { "tags": [ "Content — titles" ], "operationId": "createContentTitle", "summary": "Create an Expertise title for the Avatar.", "description": "Creates a new Expertise title under the Avatar. The only input\nis the title's name; the new title is owned by the API\ncredential that created it.\n\n**Why it matters.** Titles are the unit of knowledge\norganisation in the platform — chunks belong to a title, not\ndirectly to an Avatar. You cannot upload content\n(`POST /content/titles/{id}/upload`) without a title to put\nit under. This endpoint is the entry point for growing an\nAvatar's Expertise in a new domain.\n\n**Use cases.**\n- One-time scaffolding when a new Avatar is created —\n pre-create the titles your team intends to fill out so\n uploads can begin immediately.\n- Migration from another knowledge platform — recreate the\n source's category structure as titles.\n- Continuous content pipeline — a CMS webhook auto-creates a\n title the first time a new content category appears, then\n uploads chunks into it.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "title" ], "properties": { "title": { "type": "string", "description": "The human-readable title name." } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "id": { "type": "string" }, "message": { "type": "string" } } } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/content/titles/{title_id}": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "get": { "tags": [ "Content — titles" ], "operationId": "getContentTitle", "summary": "Get one Expertise title owned by the Avatar.", "description": "Returns the metadata of a single Expertise title owned by the\nAvatar — its name, enabled flag, and active chunk count.\nReturns 404 if the title does not exist or is owned by a\ndifferent Avatar (the two cases are intentionally\nindistinguishable, so this endpoint never confirms another\nAvatar's title id).\n\n**Use cases.** Detail view in an external CMS; integration\nthat needs to validate a `title_id` exists before uploading\ncontent; sync-checking after a `PUT /content/titles/{id}`\nupdate has been applied.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "title_id": { "type": "string" }, "title_name": { "type": "string" }, "is_enabled": { "type": "boolean" }, "count": { "type": "integer", "description": "Number of active chunks belonging to this title." } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "tags": [ "Content — titles" ], "operationId": "updateContentTitle", "summary": "Rename an Expertise title owned by the Avatar.", "description": "Renames an Expertise title. The BETA accepts only\n`title_name`; owner, weight, and description are not part of\nthe Expertise-title surface in Phase 1 and are not changeable\nthrough this route. Cross-Avatar updates are rejected with\n404.\n\n**Why it matters.** Titles are referenced by `title_id`\neverywhere — in chunks, footers, retrieval results, the\ninternal MaxGoodWeb UI. Renaming a title here updates all\nthose surfaces atomically; no further work is needed to keep\nthe rest of the platform in sync.\n\n**Use cases.** Bulk-rename titles after a content reorg; fix\na typo in a title name without recreating its chunks; sync\ntitle-name changes from an external system of record.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "title_name" ], "properties": { "title_name": { "type": "string" } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "title_name": { "type": "string" } } } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "tags": [ "Content — titles" ], "operationId": "deleteContentTitle", "summary": "Soft-delete a title and cascade to its content.", "description": "Soft-deletes a title and cascades to its chunks, documents,\nand footer: rows are marked deleted, text fields are\noverwritten with placeholders, S3 objects are retained, and\nChromaDB vectors are removed so the title is no longer\nretrievable. **This is a one-way operation.**\n\n**Why it matters.** The cascade is intentional — leaving\ntombstoned chunks behind would surface garbage through\nretrieval. Because S3 objects are retained, the originating\ndocuments are recoverable if you ever need them.\n\n**Use cases.** Retire a title you no longer want the Avatar\nto draw on; clean up after a misconfigured upload; remove\nout-of-licence content programmatically when a licence ends.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessEnvelope" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/content/titles/{title_id}/is_enabled": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "put": { "tags": [ "Content — titles" ], "operationId": "setContentTitleEnabled", "summary": "Enable or disable a title (reversible).", "description": "Sets a title's `is_enabled` flag. Disabled titles stay in the\ndatabase but are excluded from retrieval — the Avatar acts as\nif they don't exist.\n\n**Business rule.** Setting `is_enabled=true` on a title with\nzero active chunks is rejected with `409 Conflict` and the\nexplanatory message *\"Cannot enable a title with no active\nchunks. Upload content to the title before enabling it.\"* —\nan empty title would silently mislead integrators into\nthinking the Avatar can draw on it. Disabling is always\npermitted.\n\n**Why it matters.** Disabled is reversible; `DELETE` is not.\nUse this endpoint for any temporary \"do not retrieve from\nthis\" state rather than deletion.\n\n**Use cases.**\n- **A/B-test what content the Avatar uses** — disable a title\n for a window and observe whether reply quality changes.\n- **Maintenance mode** while content is being rewritten —\n disable, edit, re-upload, re-enable.\n- **Seasonal content** — disable an event-year-specific title\n after the event, without losing it for next year.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "is_enabled" ], "properties": { "is_enabled": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "title_id": { "type": "string" }, "is_enabled": { "type": "boolean" } } } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "description": "Cannot enable a title that has no active chunks.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } } } } }, "/content/titles/{title_id}/upload": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "post": { "tags": [ "Content — titles" ], "operationId": "uploadContentToTitle", "summary": "Ingest a file or raw text into a title.", "description": "Submits a file or raw text to be ingested into a title. Files\ngo through Apache Tika for text extraction (PDF, DOCX, PPT,\nand other common formats are supported); the JSON path skips\nTika. In both paths the text is chunked, embedded with the\nplatform's local embedding model, and stored in PostgreSQL\nand ChromaDB. The request body is capped at 25 MB.\n\nAn optional `source_document_title` is recorded alongside the\nresulting chunks so they can be traced back to the document\nthey came from. Arbitrary per-title or per-chunk metadata is\nintentionally not in Phase 1 — it is on the roadmap with the\nwider IDRAG metadata improvements.\n\n**Why it matters.** This is the primary path for growing an\nAvatar's knowledge. Every chunk created here becomes\nretrievable on the next `/rag-search` or `/avatar/respond`\ncall against the Avatar.\n\n**Use cases.**\n- **Programmatic sync** of a customer's internal wiki or\n playbook into their Enterprise Avatar.\n- **Auto-ingest** meeting transcripts, support tickets, or\n post-mortems as they are generated, on a queue.\n- **Bulk import on Avatar setup** — feed a folder of PDFs\n through the endpoint to bootstrap a new Avatar.\n- **Continuous content pipeline** — webhook from a CMS\n triggers an upload whenever an article is published or\n updated.\n", "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "file" ], "properties": { "file": { "type": "string", "format": "binary" }, "source_document_title": { "type": "string", "description": "Optional human-readable name for the document this upload represents; recorded on the resulting chunks for traceability." } } } }, "application/json": { "schema": { "type": "object", "required": [ "text" ], "properties": { "text": { "type": "string" }, "source_document_title": { "type": "string", "description": "Optional human-readable name for the document this text represents; recorded on the resulting chunks for traceability." } } } } } }, "responses": { "200": { "description": "OK — content ingested.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "message": { "type": "string" }, "chunk_ids": { "type": "array", "items": { "type": "string" } }, "chunk_count": { "type": "integer" } } } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" }, "413": { "$ref": "#/components/responses/PayloadTooLarge" } } } }, "/content/titles/{title_id}/documents": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "get": { "tags": [ "Content — titles" ], "operationId": "listContentTitleDocuments", "summary": "List the source documents of a title.", "description": "Lists the source documents (the original uploaded files) for a\ntitle — filename, mimetype, size, upload date, active flag,\nand a presigned `download_url` that lets you retrieve the\noriginal file directly. The presigned URL is valid for 15\nminutes; re-call this endpoint to refresh.\n\nBoth active and inactive (soft-deleted) documents are\nreturned, so a tombstoned title's references stay visible\n— `is_active=false` indicates a soft-deleted row whose S3\nobject is still retained.\n\n**Why it matters.** Documents are the *source* of chunks.\nKnowing which document a chunk came from — and being able to\nfetch the original file back — is essential for any audit,\nattribution, or retraction workflow.\n\n**Use cases.** External content-management dashboard showing\nper-title source files; an audit log of what was ingested\nwhen; resolving \"where did this chunk come from?\" while\nresponding to a content question from a customer; pulling\nthe original file back into a different system without\nkeeping a parallel copy.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "title_id": { "type": "string" }, "document_count": { "type": "integer" }, "documents": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string" }, "filename": { "type": "string" }, "filetype": { "type": "string" }, "mimetype": { "type": "string" }, "filesize": { "type": "integer", "format": "int64" }, "upload_date": { "type": "number", "format": "double" }, "is_active": { "type": "boolean" }, "download_url": { "type": "string", "description": "Presigned download URL valid for 15 minutes; empty string if the underlying storage location is unset or unreachable." } } } } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/content/titles/{title_id}/chunks": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "get": { "tags": [ "Content — titles" ], "operationId": "listContentTitleChunks", "summary": "List the active content chunks of a title.", "description": "Lists the active chunks under a title — each chunk's id, name,\ntext, and per-chunk `usage_count` (how often this chunk has\nactually appeared as retrieval context in an Avatar reply).\nInactive (soft-deleted) chunks are excluded.\n\n**Why it matters.** Chunks are the atomic units of retrieval.\nLooking at them gives you the most accurate picture of what\nthe Avatar can actually surface for this title, and\n`usage_count` is a direct signal of which chunks are doing\nthe work.\n\n**Use cases.** External chunk-level CMS; usage-driven content\nprioritisation (high-usage chunks deserve quality investment;\nchunks that never get retrieved are candidates for editing or\nremoval); identifying low-quality chunks for editing\n(`PUT /content/chunks/{id}`) or deletion.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "chunk_count": { "type": "integer" }, "chunks": { "type": "array", "items": { "type": "object", "properties": { "chunk_id": { "type": "string" }, "chunk_name": { "type": "string" }, "chunk_text": { "type": "string" } } } } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/content/titles/{title_id}/footer": { "parameters": [ { "in": "path", "name": "title_id", "required": true, "schema": { "type": "string" } } ], "get": { "tags": [ "Content — titles" ], "operationId": "getContentTitleFooter", "summary": "Get the footer for a title.", "description": "Returns the footer that the Avatar will surface alongside\nretrieved chunks from this title. Footers are typically used\nfor attribution, source citations, or licensing notices.\n\nAlways returns a `footer_html` value: when an explicit footer\nis set for the title, its HTML is returned and `footer_id`\nis the footer row's id; when no footer is set, `footer_id`\nis `null` and `footer_html` falls back to the\norganization-type-wide default footer so the caller has\nsomething usable.\n\n**Why it matters.** Footers travel with retrieval results, so\nlegal or attribution requirements stay attached to the\ncontent they apply to without polluting the chunks\nthemselves.\n\n**Use cases.** Surface a licence statement next to every\nretrieved chunk from a licensed third-party title; show an\nauthor byline; insert a \"see original\" backlink to the\ncanonical source.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "footer_id": { "type": "string", "nullable": true, "description": "Footer row id, or null when no explicit footer is set for the title." }, "footer_html": { "type": "string", "description": "Footer HTML — the title's own footer when set, otherwise the organization-type default." } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "tags": [ "Content — titles" ], "operationId": "setContentTitleFooter", "summary": "Create or replace the footer for a title.", "description": "Upserts the footer for a title. Sending the request again for\nthe same title replaces the existing footer; the footer\napplies retroactively to every existing chunk under the\ntitle and prospectively to new ones. Paired with the GET at\nthe same URL.\n\n**Use cases.**\n- Bulk-update licensing language across all titles when a\n licence's terms change.\n- Insert per-title attribution when onboarding a new licensed\n author.\n- Set a \"source: \" footer on titles backed by a\n single source document so retrieval results carry their\n origin.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "footer_html" ], "properties": { "footer_html": { "type": "string", "description": "Footer HTML content." } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "footer_id": { "type": "string" }, "message": { "type": "string" } } } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/content/chunks/{chunk_id}": { "parameters": [ { "in": "path", "name": "chunk_id", "required": true, "schema": { "type": "string" } } ], "get": { "tags": [ "Content — chunks" ], "operationId": "getContentChunk", "summary": "Get one content chunk owned by the Avatar.", "description": "Returns a chunk's text, name, parent `title_id`, and\nactive-state flag.\n\nSoft-deleted chunks are intentionally still readable here:\n`DELETE /content/chunks/{id}` overwrites the text with a\nplaceholder but leaves the row queryable so the chunk-to-\nsource-document linkage is preserved. A caller who already\nknows the chunk_id can still fetch it — `is_active=false`\nsignals the tombstoned state. The list-chunks endpoint\n(`GET /content/titles/{id}/chunks`) excludes soft-deleted\nchunks, so deleted chunks are not *discoverable* from the\nlist but remain *readable* directly.\n\n**Use cases.** Chunk detail view in an external CMS;\nresolving `partner_content_ids` returned by `/rag-search` or\n`/avatar/respond` to the underlying chunk content.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "type": "object", "properties": { "chunk_text": { "type": "string" }, "chunk_name": { "type": "string" }, "title_id": { "type": "string" }, "is_active": { "type": "boolean", "description": "false when the chunk has been soft-deleted; the returned `chunk_text` will be a placeholder." } } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "tags": [ "Content — chunks" ], "operationId": "editContentChunk", "summary": "Edit a chunk's text (and optionally its source attribution).", "description": "Edits a chunk's text and re-embeds it in ChromaDB so\nsubsequent retrievals reflect the change.\n\nEvery chunk's stored text starts with a canonical header that\nthe platform builds at upload time:\n\n```\n `Title in Avatar Expertise: `\n `Source Document Title: `\n```\n\nThat header is part of the embedding, so the title name and\nsource-document title both participate in retrieval matches\nfor every chunk. Supplying `source_document_title` here\nrebuilds the header with the new source-document title and\nupdates the chunk's stored metadata + auto-derived `name`;\nomitting it preserves the existing source attribution.\n\nCross-Avatar attempts and unknown chunk ids both return 404\n(the underlying `\"Forbidden.\"` is mapped so the endpoint\nnever confirms a chunk exists under another Avatar).\n\n**Why it matters.** Edits are surgical. You can fix a single\nbad chunk without re-ingesting the whole document.\nRe-embedding is automatic, so retrieval quality stays current\nwith the edit.\n\n**Use cases.**\n- Correct a fact in a chunk without re-uploading the source\n document.\n- Sharpen a chunk's wording — Tika extraction can produce\n slightly awkward English; a brief edit improves both the\n embedding (and therefore retrieval) and the eventual reply\n quality.\n- Re-attribute a chunk to a different source document (for\n example, after merging or renaming source documents) and\n have the new attribution flow through to retrieval.\n- Redact PII discovered post-ingest in a single chunk rather\n than rebuilding the whole title.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "chunk_text" ], "properties": { "chunk_text": { "type": "string" }, "source_document_title": { "type": "string", "description": "Optional. When provided, rebuilds the chunk's canonical header (which is part of the embedding) with this new source-document title and updates the chunk's stored metadata + auto-derived name column." } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessEnvelope" } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "tags": [ "Content — chunks" ], "operationId": "deleteContentChunk", "summary": "Soft-delete a chunk.", "description": "Soft-deletes a chunk: text is replaced with a placeholder,\nthe `is_active` flag is flipped, the ChromaDB vector is\nremoved, and an audit record is written. The chunk's database\nrow is preserved so its linkage to the originating source\ndocument survives. Cross-Avatar attempts and unknown chunk\nids both return 404 (the underlying `\"Forbidden.\"` is mapped\nso the endpoint never confirms a chunk exists under another\nAvatar).\n\n**Use cases.** Drop a single bad chunk without affecting the\nrest of its title; one-off content takedown driven by an\nexternal review workflow; programmatic redaction tied to a\nprivacy report.\n", "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessEnvelope" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/avatar/respond": { "post": { "tags": [ "Avatar reply" ], "operationId": "avatarRespond", "summary": "Get an Avatar reply on behalf of a member.", "description": "Asks the Avatar to reply to a message on behalf of one of its\nmembers. The caller names the member by `mg_user_id` (who\nMUST be a current member of the Avatar bound to the API key —\n404 otherwise); per-thread context is built automatically\nfrom that member's stored interaction history, so the\nintegrator does NOT manage threads.\n\n**Why it matters.** This is the only BETA endpoint that\nactually *exercises* the Avatar's coaching behaviour from\noutside the first-party MaxGood.work channels. It runs the\nsame code path that powers the website chat, Slack, Teams,\nDiscord, and email integrations — reply quality, retrieval,\npersonalisation by member tags, and tone are identical to\nwhat the member would get through any of those.\n\n**The interaction id is server-minted.** The endpoint\ndeliberately does not accept a caller-supplied interaction or\nconversation id, because the underlying chat-core upserts by\nid with no ownership predicate — a caller-supplied id would\nbe a write-IDOR onto any interaction in the system.\n`interaction_id` in the response is for reference only.\n\n**Prompt-length limits (Phase 1).** Two hard caps are\nenforced *and* both must pass:\n\n- **10 000 characters** (`len(prompt)`) — matches the limit\n MaxGoodWeb's own chat input enforces.\n- **4 096 tokens** (counted with the same tokenizer the LLM\n uses) — the LLM context-window unit.\n\nFor typical English prose either cap leaves plenty of room\nand 10 k characters is the binding constraint; for unusually\ndense text (code blocks, CJK, lots of symbols) the token\ncap can bite first. **Either over-limit rejects the request\nwith `400`** and a message naming the cap that was hit, so\nintegrators are encouraged to surface *both* limits in their\nown UX (a character counter is cheap; an approximate\ntoken-count is a useful \"yellow zone\" indicator).\n\nBoth limits will become per-Avatar-type configuration in a\nfuture release (tracked under the per-Avatar-type-limits\nExpedite card linked from card #65).\n\n**Use cases.**\n- **Embed the Avatar in a customer's own product surface** —\n their support widget, in-app coach, intranet chat —\n without taking on the full first-party integration.\n- **Asynchronous coaching loops** — a daily job picks up\n trigger conditions for a member, drops a prompt to the\n Avatar, and emails the reply through your own mail system.\n- **Bridge to a private channel** that MaxGood.work itself\n does not have a first-party integration for yet (for\n example, a closed enterprise messenger).\n- **A/B-test reply presentation** — wrap `/avatar/respond` in\n your own UI and try formatting variants (with or without\n references, summarised, etc.) while the Avatar's logic\n stays constant.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "mg_user_id", "prompt" ], "properties": { "mg_user_id": { "type": "string", "description": "Internal MaxGood user id of the end user this request is on behalf of. Must be a member of the Avatar bound to the API key." }, "prompt": { "type": "string", "description": "The user's message (non-empty after trim)." } } } } } }, "responses": { "200": { "description": "OK.", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/SuccessEnvelope" }, { "type": "object", "properties": { "response": { "$ref": "#/components/schemas/AvatarReply" } } } ] } } } }, "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "description": "API credential is missing the required `enterprise_api` scope.", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } }, "404": { "description": "User is not a member of this Avatar.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } }, "502": { "description": "The chat core did not return a response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FailureEnvelope" } } } } } } } } }