| POST | /api/start/spaces | GitHub human session, same-origin browser | Create one private Mainmind-managed space with {name, purpose?, template?, idempotency_key}. template is blank (the default), business, job-hunt or project; it adds a few example pages the space then owns. The same key and details resume the original creation; changed details conflict. The signed-in person becomes its Founder. No GitHub repository or App installation is created. Storage and serving readiness are separate; a retry never silently replaces saved data. | {ok, state, workspace, name, purpose, template, storage, replayed, message?, retryable?, commit?, open_url?}. 200 ready; 202 creating or reading; 409 conflict or recovery required. |
| GET | /api/start/spaces | GitHub human session | Read your original space creation with query creation_key. Rechecks current Founder membership and serving evidence. A creation key is a retry reference, not an access credential. | The bounded creation status for the current person only, or a refusal. Cache disabled. |
| POST | /api/machine-enrollment | sponsor invitation | Claim a pending machine member in the explicit workspace query using an Authorization Bearer enrollment token and JSON {credential}. The runner creates its credential locally; the server stores only digests, rechecks the live sponsor and access policy, and activates the same member atomically. A client registration alone grants no membership. | Credential-free machine activation receipt, or a refusal. An exact retry with the same credential is recoverable only during the original invitation window. |
| GET | /api/tool-gateway/v1/connections | Mainmind lease with connections.use | List only the active Connection aliases, read/write modes and kind granted to this exact lease. api-key and oauth2-refresh Connections also include the non-secret credential_generation used for write-only replacement. The list exposes no origin, token endpoint, descriptor internals or credential values. | {connections:[{alias,modes[],kind,credential_generation?,slots?}]} for the lease's workspace and generation-bound capabilities. |
| GET | HEAD | /api/tool-gateway/v1/connections/:alias/* | Mainmind lease with the exact Connection generation's read capability | Provider-neutral read transport for an installed Connection. The local tool chooses an alias and relative path only. Mainmind selects the immutable server-installed API origin. oauth2-refresh/v1 uses the installed HTTPS token endpoint and injects the sealed OAuth credential and optional tenant binding; api-key/v1 injects the sealed static key into the installed authentication header and does not call a token endpoint. Then it strips caller credentials and forwarding headers, refuses redirects and traversal, bounds both bodies, and blocks exact credential reflection. | The provider status and installed safe response headers, with no lease, provider token, refresh token, client secret, API key or redirect location. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/v1/connections/:alias/* | Mainmind lease with the exact Connection generation's write capability | Provider-neutral mutation transport for the same installed Connection. Every non-read method requires the generation-specific write grant and a durable run-ledger attempt before network egress. Installation is technical access only; the space's Process and Authority still govern the business act. | The bounded safe provider response. A lost response after egress is recorded as partial and requires provider-state reconciliation. |
| GET | HEAD | /api/tool-gateway/zoho-inventory/* | Mainmind lease with zoho.inventory.read | The read half of the full Zoho Inventory adapter used by the unchanged local CLI. Replaces the lease and every caller organization selector with server-held credentials and the configured Zoho organization, and can reach only its configured Zoho data-centre Inventory API host. | The streamed Zoho response with status and safe response headers. No configured refresh token, access token or organization selector is returned by the gateway. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/zoho-inventory/* | Mainmind lease with zoho.inventory.write | The mutation half of the same full Zoho Inventory adapter. Forwards JSON or multipart bodies up to 16 MiB plus safe conditional and idempotency headers; requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Zoho response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/zoho-desk/* | Mainmind lease with zoho.desk.read | The read half of the full Zoho Desk adapter used by the unchanged local CLI. The CLI points ZOHO_DESK_BASE_URL here and presents the lease as its access token; the gateway swaps in the server-held OAuth token, injects the configured orgId header, and can reach only its configured Zoho data-centre Desk API host. | The streamed Zoho Desk response with status and safe response headers. No configured refresh token, access token or space id is returned by the gateway. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/zoho-desk/* | Mainmind lease with zoho.desk.write | The mutation half of the same full Zoho Desk adapter — the transport that lets daily-ticket-triage save a verified draft. Forwards JSON or multipart bodies up to 16 MiB plus safe conditional and idempotency headers; requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Zoho Desk response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/shopify-admin/* | Mainmind lease with shopify.admin.read | The read half of the full Shopify Admin adapter used by unchanged local tools. The tool points SHOPIFY_STORE_URL here and sends the lease as x-shopify-access-token; the gateway swaps in the server-held static admin token and can only ever reach the configured *.myshopify.com store. No OAuth machinery: nothing is minted, cached or replayed. | The streamed Shopify response with status and safe response headers. The configured admin token and store selection never leave the gateway. |
| POST | /api/tool-gateway/shopify-admin/admin/api/:version/graphql.json | Mainmind lease with shopify.admin.read or shopify.admin.write, chosen by the document | The Admin GraphQL endpoint rides POST, so the gateway classifies the parsed document instead of the method: a query with no mutation or subscription keyword outside strings and comments uses the read capability; anything else — including an unparseable or oversized body — requires write. Over-asking on an ambiguous read refuses safely; the reverse would let a mutation ride the read grant. POST /graphql.json and POST /graphql rewrite to /admin/api/2025-01/graphql.json before that classification, so a documented relative path is not a REST write 404. | As above. |
| POST | /api/tool-gateway/shopify-admin/graphql.json | Mainmind lease with shopify.admin.read or shopify.admin.write, chosen by the document | Short form of the Admin GraphQL endpoint. Rewritten to /admin/api/2025-01/graphql.json, then classified by the document as the versioned route is. | As above. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/shopify-admin/* | Mainmind lease with shopify.admin.write | The mutation half of the same full Shopify Admin adapter (REST writes, and GraphQL documents the classifier refuses to call reads). Requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Shopify response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/amazon-sp/* | Mainmind lease with amazon.sp.read | The read half of the full Amazon Selling Partner API business/data-plane adapter. Replaces the lease with a server-held LWA token or an exact-resource server-held RDT and can reach only the configured regional SP-API host. | The SP-API response with safe status and rate-limit headers. Restricted tokens and provider-signed artifact URLs stay sealed; model-named document fields become narrow Mainmind artifact locators. |
| POST | /api/tool-gateway/amazon-sp/batches/* | Mainmind lease with amazon.sp.read | The batch-read exception: only the exact batch paths named in AMAZON_SP_BATCH_READS use the read capability even though Amazon expresses them as POST. The body is bounded at 128 KiB. | As above. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/amazon-sp/* | Mainmind lease with amazon.sp.write | The mutation half of the full SP-API business/data-plane adapter, except the named POST batch reads and the provider credential-control plane. Forwards bodies up to 16 MiB and provider-modeled conditional, idempotency and signature headers to the fixed regional host; requires a durable run-ledger attempt before sending. LWA, restricted tokens, signed destinations and Services encryption material stay sealed server-side. Client-secret rotation is refused until Mainmind owns its queue-to-sealed-credential lifecycle. | The safe SP-API response, a managed-token alias, or an opaque Mainmind artifact locator. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | PUT | /api/tool-gateway/amazon-artifacts/:handle | one opaque run-bound artifact locator | Transfers exactly one provider-issued Amazon document or upload destination without revealing its signed URL, headers or encryption material. A download accepts GET/HEAD; an upload accepts one raw PUT. The locator rechecks its owning lease, live grant, open run and capability; upstream redirects, private hosts, caller credentials and arbitrary methods are refused. Services uploads are encrypted at the trusted edge. | The streamed artifact response with safe media, range and disposition headers. The opaque locator itself grants no provider API access and expires no later than the provider destination. |
| GET | /api/tool-gateway/knowledge/records | Mainmind tool lease | The named knowledge.records.read adapter. Query: prefix= repeated, each naming one Record Kind such as records/products. Returns the space's own Records as one bundle, so a tool that needs them can run on a lease whose checkout carries no knowledge. GET only, records/<kind> only with no traversal, and filtered by the live knowledge scope of the member the lease belongs to — the same gate read_node uses, recompiled per request rather than read from the lease. It grants no access that member did not already have; only the transport differs. | {version, source, commit, prefixes[], records{path: raw Markdown}} at the serving projection commit. Bounded at 8 prefixes, 2000 Records and 8 MB, and a node projected without content is a 503 — every limit refuses rather than truncating, because a scan handed half a catalogue reports a clean business. |
| POST | /api/tool-credentials | bearer, owner or co-founder session | Place one vendor credential for a workspace. Body: {workspace?, capability, credential}. The provider credential is create-only under its read capability and backs separately leased read/write transport; D1 first reserves the unique workspace/capability pair, then a fresh AES-256 data key encrypts the normalized credential under AES-GCM and the root key wraps that data key with AES-KW before the ciphertext reaches KV. A second placement returns 409; change a placed credential with PUT. There is deliberately NO GET for the value: once placed, a credential is reachable by its adapter and by no surface a person or agent can call. An owner session placing from Setup passes the same create-only gate as the machine bearer. | {ok, workspace, capability, fields[]} naming the fields stored and never their values; {error} when the capability is unknown, the credential malformed, storage unavailable, or a credential is already configured. |
| PUT | /api/tool-credentials | bearer, owner or co-founder session | Replace some of a placed vendor credential's values in place, such as a rotated Amazon client secret. Body: {workspace?, capability, credential, expected_generation}. credential holds only the fields that change; every other stored value is kept, because sealed values cannot be read back to type again. expected_generation is the slot's generation from /api/tool-credentials/status; the generation moves by one, so of two concurrent replacements one wins and the other gets 409. The account fields (Amazon region, Zoho dc and organization_id, Shopify store_domain) must stay the same, so a replacement never points the slot at another account. Access tokens minted from the old values are forgotten, and the next call mints from the new ones. A slot whose first placement never sealed takes the whole credential, and so does a second replacement within five minutes of the last, because KV may still return the values it replaced (409 otherwise). A minting secret is checked with the provider before it is saved; a refusal keeps the previous login. Zoho accepts credential.authorization_code when the client is already saved: Mainmind exchanges the one-time code and stores the refresh token, never the code. | {ok, workspace, capability, generation, fields[]} naming the fields replaced and never their values; {error} when a field is unknown, an account field differs, the provider refuses the new login, the generation moved, or storage is unavailable. A refused replacement does not move the generation. |
| POST | /api/tool-credentials/amazon/sign-in | owner or co-founder session | Start Sign in with Amazon for the built-in Amazon Selling Partner account, through Amazon's SP-API website authorization workflow. Body: {workspace?, store, app_id, client_id?, client_secret?}. store is one of the Seller Central stores (us, ca, mx, br, uk, de, fr, it, es, nl, se, pl, be, tr, ae, sa, eg, in, jp, au, sg) and sets the region; app_id is the app's amzn1.sp.solution. ID. client_id and client_secret are required when no Amazon account is stored; when one is, a missing one is taken from it server-side and never returned, and a store in another region is refused. Parks a single-use state for 15 minutes, bound to this browser session, with the app's values sealed and the account's current generation recorded; expired states are deleted. A stored account that cannot be opened refuses with 503. A browser session only: Amazon returns to this browser to finish. | {authorize_url, redirect_uri, login_uri}: the Seller Central consent address to send the browser to, and the two addresses the app in Seller Central must list as its Redirect URI and Login URI. {error} in plain words otherwise; never a value. |
| GET | /api/tool-credentials/status | bearer, owner or co-founder session | Which provider-credential slots exist for a workspace and what state each is in — presence only, so Setup can show the vault without weakening it. placed is the registry reservation; sealed says whether the ciphertext is actually present (true), missing after a lost placement (false), or unverifiable because KV was unreachable (null) — a reservation is never presented as a credential. registered_at is when the registry first recorded the slot, which for credentials placed before the registry existed is the backfill date, and placed_by names the actor a browser or bearer placement recorded (null for backfilled rows). options carries the closed server-side enums a form may offer, account_fields the fields a replacement must keep, and generation, replaced_at and replaced_by the last in-place replacement (generation 0 and nulls when never replaced). The Amazon slot also carries client_secret_due_at: Amazon expires an app's client secret 180 days after it is made, so this is 180 days from when the secret was saved here, the latest it could be due; a replacement that changes client_secret restarts it. Also lists the workspace's active installed Connections by alias. The sealed values themselves remain unreadable on every surface; this route returns no credential material, no field values and no provider hosts — the sealed check reads KV key names only. | {workspace, registry_complete, credentials:[{capability, write_capability, provider, fields[], secret_fields[], account_fields[], options{}, placed, sealed, registered_at, placed_by, generation, replaced_at, replaced_by, client_secret_due_at?}], connections:[{alias, created_at}]}. |
| POST | /api/tool-connections | bearer, owner or co-founder session | Install one create-only provider-neutral Connection for a workspace. Body: {workspace?, alias, profile, credential?}. Omit credential to install the Connection pending: the response carries a single-use placement_url (seven days) at which a Founder or co-founder of the space pastes the values in a browser after seeing the exact destination; GET /place/<key> shows that page and POST /place/<key> seals the values, flips the Connection active and burns the key. alias is kebab-case (google-ads); underscores are refused. profile must name schema mainmind.connection-profile/v1, a lowercase id, and driver api-key/v1, sign-in/v1, secret-slots/v1 or oauth2-refresh/v1. oauth2-refresh/v1 may declare slots, which without lease_env must each fill a request header through header_slots; inject_access_token is refused, because no tool lease carries a Connection's values. secret-slots/v1 may be installed; a permit still omits its values. A 400 names the failing field and the accepted shape, points at https://mainmind.app/docs/mcp-tools#install-provider-connection, and never echoes credential values. This trusted control-plane act binds api-key/v1 (a fixed HTTPS API origin and authentication header, with a server-held api_key), sign-in/v1 (a login URL on the API origin and the sealed login values) or oauth2-refresh/v1 (HTTPS token URL, API origin, relative API root, header policy, body bounds, optional header_slots and lease_env) together with the sealed credential into immutable generations. A checked-out tool and its Git-authored code cannot alter these trust anchors, and there is deliberately no credential GET. | {ok, workspace, alias, profile_id, profile_generation, connection_generation, credential_generation, modes, fields, kind} with field names only and no credential values; without credential {ok, status: pending, fields, destination, placement_url, placement_expires_at}; 409 when an active alias, or an unexpired pending one, already exists; 400 names the failing profile or credential field and the accepted shape. |
| POST | /api/tool-connections/:alias/place | bearer, owner or co-founder session | Place the values on a pending Connection by alias, for a signed-in Founder or co-founder who does not need the placement link. Body: {workspace?, credential}. The same refusal and sealing path as the placement page; the Connection becomes active and its placement link stops working. | {ok, alias, status: active, credential_generation, fields, modes}; 404 when the alias is not pending; 410 when it is pending but its placement week has passed, naming that reason (install again under the same alias for a fresh link); 409 when an active Connection took the alias meanwhile; 400 names the failing credential field. |
| DELETE | /api/tool-connections/:alias | bearer, owner or co-founder session | Revoke one active or pending Connection. The sealed values are never read; the row stays as the audit record and the alias is free for a fresh install. | {ok, alias, previous_status, revoked_at}; 404 when no active or pending Connection carries the alias. |
| POST | /api/tool-connections/:alias/retry-sign-in | bearer, owner or co-founder session | Try the password already stored for one sign-in account, once. No new password is accepted and the token is not returned. A 401 or 403 locks the account again, unless the answer says the problem is the token rather than the password, and that status and a short message are recorded. A 401 that says invalid token, a 429, a 5xx, a timeout or a network error clears a lock and does not lock the account. An owner or co-owner only. The same act is the MCP tool retry_provider_sign_in; a member key cannot call this HTTP route. | {ok, alias, last_login_at} when the stored login is accepted; 409 when the account refuses the password, with provider_status and provider_message; a further call after that refusal is 409, says the login was not tried, and does not contact the provider; 502 when the login is busy, unreachable, or says the problem is the token, without locking; 403 for anyone who is not an owner or co-owner; 400 when the account is not a sign-in. |
| PUT | /api/tool-connections | bearer, owner or co-founder session | Replace an api-key/v1 or oauth2-refresh/v1 Connection credential write-only. Body: {workspace?, alias, expected_credential_generation, credential}; credential is {api_key} for api-key/v1 and the full install credential (client_id, client_secret, refresh_token, tenant_id when bound, every declared slot) for oauth2-refresh/v1. alias is kebab-case. The installed destination and profile stay immutable. A 400 names the failing field and the accepted shape and points at https://mainmind.app/docs/mcp-tools#replace-provider-connection-credential. A stale expected_credential_generation returns 409. oauth2-refresh replacement reaches this route only through the deployment bearer, a Founder owner session or a co-founder session; a bound tenant_id must be the installed one. Old Connection permits cannot use the replacement. No credential GET exists. | {ok, alias, kind, credential_generation, fields, ...} with generation locators only; no old or new values. |
| GET | /api/access | bearer | The access requests waiting on you, newest first. The founder's own surface. | {requests[]} with created_at, email, note, source and status. |
| GET | /api/oracle/voice | member browser session | Same-origin WebSocket upgrade for one live voice call. Query: workspace, random session_id, optional conversation_id and previous_session_id. The edge resolves the linked GitHub member and immutable epoch, then derives an internal session object name that browser input cannot retarget. Microphone PCM streams continuously, including pauses, to Cloudflare Flux while the call is enabled. Flux detects turns and interruptions. Each transcript uses the same bounded conversational agent and OrganizationRuntime ledger as typed Oracle; the complete answer is committed, reread and source-validated before sentence and Aura PCM streaming begins. One active call per member epoch, thirty call admissions per rolling hour and a ten-minute call limit. Mainmind saves no raw or generated audio and disables the SDK transcript store. | Voice protocol JSON, interim and final transcripts, private conversation snapshots and raw 16 kHz PCM audio on the initiating WebSocket. No cross-connection broadcast. Interrupting cancels active answer and speech work. |
| POST | /api/oracle/voice | member browser session | Legacy same-origin JSON speech route. operation=transcribe takes request_id, base64 audio (at most 750000 decoded bytes), mime_type and workspace; operation=speak takes request_id, conversation_id and turn_id. The browser uses speak only for an explicit Read aloud replay, which never opens the microphone. Cloudflare Whisper transcribes; Cloudflare Aura speaks a bounded excerpt of a current owned sourced answer. No arbitrary client text is synthesized. Admission is limited per live membership to thirty recordings and sixty playbacks per rolling hour. Mainmind does not persist raw or generated audio. No automatic audio retry; access/source/deletion checks run before return. | Transcription {text}, private audio/mpeg, or an explicit error. A transcription does not itself submit a durable question or authorize work. |
| GET | /api/oracle | member browser session | Read private durable conversations for the selected space and live membership. Query: conversation_id?. Source-bearing history is withheld when current access or source revisions change. | {conversations,conversation,capabilities,notice,retention_days}. An active typed turn survives browser reconnection. Live microphone capture uses the WebSocket voice route; explicit Read aloud replay uses its bounded POST compatibility route. |
| POST | /api/oracle | member browser session | Same-origin JSON. Operations: create, send, cancel, delete, action, feedback, metrics. Stable request_id for create/send/action. Workers AI can answer conversationally or choose scoped search, read_node, find_process, waiting_questions and read_question tools, using bounded private history. Its one write is record_answer: the Founder's own answer to a waiting ask, through decide, where the words are the Founder's message in that turn and the server records only a yes, no or settled that message plainly says (or a changes note), one per turn, for an ask looked up that turn. Explicit source-bound comments/requests use existing working state; no canonical policy or human ruling is executed. Conversation-only retention: 30 days. Feedback follows the existing private issue lifecycle. Metrics are content-free and Founder-only. | Conversation, accepted turn, verified working-state result or feedback receipt. No caller-supplied actor, role or workspace retargeting. |
| POST | /api/knowledge-answer | bearer or session | Explicitly answer a question using bounded authorized source excerpts through Workers AI. Body: query, context?, workspace?. Existing search and guide do not send source bytes for inference. Does not save, queue or execute work. | {hits,guide,answer,serving}. Answer labels source summaries/inferences, citations, gaps and conflicts; no invented source links. Invalid/unavailable inference retains sources. Final membership and source revision are rechecked. |
| GET | /api/boot | member credential | Start as this member. Query: workspace?, session_kind?, seat?, full?. A seat that resolves returns the same summary MCP start does (the seat first, AUTHORITY.md inline up to 6 KB and the other entry documents named, each space-wide list at its count and first three, and summary); full=true returns it whole. The same organizationBoot as MCP start: identity, entry documents, attention, Process count, recorded team, your_seat, colleagues, and bounded restore (inbox, open_asks with open_asks_complete beside it, open_runs locators; never a control_key), with open_asks_total and open_runs_total naming how many of each are open for this caller, so a truncated list says what it was cut from; each is null, and restore_error true, when the restore read failed, since a zero there would be a count nothing took, while a queue that was read and is empty is still 0. An open ask carries changes_note and changes_at for the one member its name resolves to, or changes_notice when the caller's own display name is shared and the note is withheld, exactly as MCP start discloses them. A Founder, and a co-founder who can already open that ask at /d/, receives becomes on each open_asks item; other roles keep the locator fields only. An open ask a Founder reopened with decide changes also carries changes_note and changes_at for the member who raised it, whatever their role, so a proposer who cannot open /d/ still reads the note; in start restore the note is disclosed only where the ask's asked_by name resolves, across every member this workspace has ever had — revoked and not-yet-redeemed rows included — to exactly one member who is this live caller and who holds the ask's knowledge scope when it has one. A name two members share, and a name the server itself defaulted, disclose nothing to anyone, including the member who really raised the ask. A missing team, or an index with no frontmatter team declaration, is a restore gap under Needs attention, with the schema at https://mainmind.app/docs/agent-team.md. An index with agent-team: [] is a complete, deliberately empty team and returns declared_empty: true; an undeclared empty index names the frontmatter Role-path dialect rather than implying seats from a body table. feedback_notices lists this member's receipts that moved to shipped, closed, or answered since they last saw that change, once, and, with change replied and new_replies, up to five reports this member filed, answered or opened that have unread messages from the builders or other agents, until the conversation is opened. The deployment bearer is refused. A malformed workspace slug is rejected, never defaulted onto another space. | {text,organization_state,reasons,process_paths,process_commands,team,your_seat,colleagues,summary?,inbox,open_asks,open_asks_more,open_asks_complete,open_asks_total,open_runs,open_runs_more,open_runs_total,restore_error,to_recover?,feedback_notices,feedback_notices_more,identity{member,name,role,workspace}}. to_recover is present only when work addressed to this member has a lapsed hold, as MCP start returns it. Ready spaces also receive ORG.md and the entry documents ORG.md declares always-read, in text. process_commands names the caller's in-scope seat Processes and each portable CLI, or explicit absence. colleagues is the ordinary directory of non-revoked live members without invite codes, credentials, credential presence or knowledge-scope grants; optional live harness from a recorded start is omitted when unknown. Open asks come back oldest filed first, so what the openAsks bound drops is the newest. open_asks locators are key, ask, url, created_at, asked_by; a Founder and a co-founder who can already see that ask at /d/ also receive becomes. An open ask a Founder reopened with decide changes also carries changes_note and changes_at for the member who raised it, whatever their role, so a proposer who cannot open /d/ still reads the note; in start restore the note is disclosed only where the ask's asked_by name resolves, across every member this workspace has ever had — revoked and not-yet-redeemed rows included — to exactly one member who is this live caller and who holds the ask's knowledge scope when it has one. A name two members share, and a name the server itself defaulted, disclose nothing to anyone, including the member who really raised the ask; a caller whose own display name is that shared name gets changes_notice on the item instead, with no byte of the note. |
| GET | /api/page-work | member credential | Read bounded queued work or reported outcomes on accessible pages. Query: status?,kind?,limit?,cursor?,inbox?,for_agent?,workspace?. Same listPageWork as MCP page_work. The deployment bearer is refused. | {items,next_cursor,total,notice}; summaries and stable identities only, no full pages. total is the size of the inbox this page was cut from, or null wherever no list is held whole to measure it: a discovery listing without inbox and a work Kind gap. Exactly as the MCP tool reports it. |
| GET | /api/decisions | member credential | Where this member's questions to the person stand. Query: key?, workspace?. Same read as MCP decisions: without key, every question this member asked that still waits on the person, every answer from the last 7 days, and recent decisions kept in the core area when this credential can read that area, including a weigh the owner answered with decide, each with the next step; with key, that one decision (its own key or the weigh_id of a choice the owner answered with decide, when it is this member's, kept in the core area, or one the person behind the credential may open on its decision page; a bundle part answers for its bundle). Another area stays with the member who asked. The deployment bearer is refused. | Without key: {waiting:{total,items}, answered:{total,since,kept,items}, person, text}. With key: {decision, text}; decision.state is waiting, changes, answered, went_ahead or retired (a read-only GitHub pull-request ask); decision.shared is true when it is someone else's decision kept in the core area. 400 for a malformed key, 404 for a decision this credential may not see, 503 when it could not be read; nothing changes either way. |
| GET | /api/work-context | member credential | Read one existing page request, its execution attempt, portable checkpoint and linked discussion. Query: path, id (canonical asg-... or legacy positive integer), cursor?, workspace?. Same workContext as MCP work_context. The deployment bearer is refused. | {item,execution,context_refs,current_source_commit,source_access_scope} with saved progress, current-access reference reconciliation and discussion. source_access_scope is the already-authorized current page's compartment, or null when unclassified. |
| POST | /api/work-session | member credential (teammate+) | Claim, checkpoint or hand off an addressed request. Body: {path,id (canonical asg-... or legacy positive integer),action,version,source_commit,idempotency_key,run_id?,control_key?,attempt_id?,checkpoint?,outcome?,reconciliation?,workspace?}. Same workSession as MCP work_session; execution actions need an owned run_id and control_key from run_start. The deployment bearer is refused and is not a start identity. | {item,execution,context_refs,current_source_commit,replayed?}. |
| GET | /api/search | bearer or session | Search the space in plain language. Query: workspace?, q, kind?, guided=1?, context? (bounded trail of previously submitted user questions). Questions and context are limited to 300 characters each. Ordinary search uses hybrid retrieval. Guided search uses Workers AI only to refine the submitted question once; document ranking and exact excerpts stay inside Mainmind. Supports title/topic and recent-document exploration, with current workspace, projection and live member access checked before returning results. Read-only; it cannot change knowledge or start agent work. | {hits[{path, kind, title, status, doc_class, snippet, exact?, decision_key?, updated_at?, document_date?}], serving, degraded?, guide?}. exact is true when a hit holds q as written, anywhere in the page, its title or path, ignoring capital letters and line breaks; a recorded decision also carries decision_key, the key to pass to decisions, and exact is taken from the person's words. Guided search leaves exact out on a file hit whose words it cannot check. Other pages may still mention it, because search ranks by words and meaning instead of listing every page that holds them. At most 20 hits come back. guided=1 adds guide:{status:ready|unavailable|no_match, summary, sources:[{path,title,quote}], suggestions:[string], query}. Quotes are literal passages from accessible documents; the summary is an orientation, not model-written business advice. No document content or metadata is supplied to the guide model. Unavailable refinement preserves original-query retrieval and authorized literal passages, with guide.refinement=unavailable when a source guide is available. Incidental matches and absent pages are not represented as an answer. Ordinary degraded lexical search remains identified. |
| GET | /api/node | bearer or session | Read one projected knowledge item for the owner app. Query: workspace?, path. Freshness is proved before content is returned. A declared decision shape is rendered by the same canonical renderer used on the ruling page, and the same derived plain-language view the explain_node tool serves rides along so the app leads with it. | One node with path, kind, title, status, excerpt, frontmatter, content, source_commit, blob_sha (the exact bytes this caller read; required as expected_blob_sha for a Page edit), access_scope, write_class, optional inert diagram_html and optional human_view; 404 with suggestions when absent. |
| GET | /api/page-collaboration | member or owner session | Read ordinary page collaboration under live page access and freshness. Query: workspace?, path, cursor?. Working state, never authority. | {items,next_cursor,source_commit,can_write,notice} |
| POST | /api/page-collaboration | teammate+ session | Create comment/task/request. Body: {path,kind,body,idempotency_key,source_commit?,block_id?,origin_id?}. Identity is session supplied; workspace cannot differ from the page. No automatic execution. | {item,replayed?,notice} |
| POST | /api/page-collaboration/:id/update | teammate+ session | Update task/request with optimistic concurrency. Body: {path,version,status,outcome?}. Completion needs an outcome; requests are explicitly claimed first. Cross-page ids refuse. | {item,notice} |
| GET | /api/notes | bearer or session | Open notes against the standing knowledge. Query: workspace?, path?; the machine bearer may additionally filter status for operational review, while browser sessions remain open-only. Never served by any read path an agent uses. Proves projection freshness first, as list_notes does; a stale or building projection is a named refusal. | {notes[]} with kind, claim, quote, who left it and when; a browser session also receives mine for its own notes. |
| POST | /api/notes | bearer or teammate+ session | Record a note. Browser body: {workspace?, path, kind, claim, quote?}; session identity supplies the author and live knowledge access. Bearer callers retain {run_id?, by_member?, by_label?}. Changes no document. | {ok, path, kind}, or {error} naming the six permitted kinds. |
| POST | /api/notes/:id/retract | bearer or teammate+ session | Withdraw a note. Browser body: {workspace?}; session identity supplies the author and may withdraw only its own attributable note. Bearer body may carry member for the existing machine contract. A ruling settles the rest. | {ok, id}, or {error} when the note belongs to somebody else, has no attributable author, or is already settled. |
| GET | /api/page-media | owner/member browser session | Read a retained image or MP4 video pinned by a readable Page block. Query: workspace?, path, block, sha256. Both the Page and original source register cross live access and freshness gates, including final disclosure and deferred stream opening. No arbitrary URLs or bearer credentials. | PNG/JPEG/WebP/GIF image or MP4 bytes with private no-store, same-origin policy, safe MIME and byte-range playback; no bytes on an inaccessible Page/source, changed fingerprint or changed access/publication. |
| POST | /api/page | teammate+ browser session | Save one Page through the same writer as keep_page. Same-origin application/json, at most 256 KB. Query: workspace?. Body: {path,operation_key,expected_blob_sha,edits?}; create with expected_blob_sha:null plus title/page and optional parent/purpose/summary/access_scope. Updates require the exact blob_sha read by this author and 1–64 native edits. Session supplies actor and live role; body cannot select another space or a run. Deployment and member bearer credentials are refused. Live access and canonical freshness are checked under the write lease. | {saved,path,url,commit,blob_sha,operation_key,current,content?} or an explicit refusal/conflict/uncertain result. Recovery preserves the original save commit. A confirmed save remains confirmed while projection is pending; content returns only when its saved version is freshly readable. Keep the same operation key and unchanged arguments for recovery. |
| GET | /api/feedback | bearer | Product feedback left by connected agents. Query: workspace?, status?, limit?. Read at the start of every improvement pass; never served by any knowledge read path. | {workspace, feedback[]} with kind, tool, message, who left it and when. |
| POST | /api/feedback | bearer | Record product feedback from a runner and automatically open or link a private issue in Mainmind's product repository. Body: {workspace?, kind, message, tool?, run_id?}. The D1 receipt commits first; GitHub delivery has a six-second deadline and failure cannot discard it. Bounded at 12 reports per connected member and 50 per workspace in a rolling hour. | {ok, id, kind, github{status, attempted_at, issue_number?, issue_url?, reason?}}, or {error} naming the four permitted kinds, refusing credential-like text, or naming the rate limit. |
| POST | /api/feedback/:id/close | bearer | Mark one feedback item handled. Body: {workspace?, reason?}. The reason survives on the row, so a later pass can see what was already answered. | {ok, id}, or {error} when it is already closed or not found. |
| POST | /api/feedback/:id/sync | bearer | Retry private GitHub issue delivery for one already-durable feedback receipt. Body: {workspace?}. Exact repeated reports link to the first still-open issue rather than opening another. | {status, issue_number?, issue_url?, reason?}; missing when the receipt is not in that workspace. |
| GET | /api/strain | bearer or session | What the knowledge is struggling with: contested documents, Lessons waiting on review, documents with a decision pending, and ledger reach over 28 days on those paths. Query: workspace?. Proves projection freshness first, as under_strain does; a stale or building projection is a named refusal. | {workspace, contested[{path, notes, kinds, latest, oldest, reach{runs,asks,notes}}], waiting{receivers[{path, lessons, oldest, titles, reach}], unresolved[], total}, proposed[{path, key, ask, reach}], reach{window_days, since, exposure, areas[{path,runs,asks,notes}], by_path}}. |
| GET | /api/space-health | bearer or session | Space health for the caller: open-note count and age, what is waiting, and pages that look out of date, repeated, or replaced while still in use. Query: workspace?. The same read as space_health. Proves projection freshness first; a stale or building projection is a named refusal. A caller sees only the areas they can read. | {workspace, open_notes{count, oldest_at, age_days}, under_strain, stale_docs[{path, title, reason, detail}], stale_truncated, digest[{key}], last_cared_at}. |
| GET | /api/why | bearer or session | Why is this the way it is. Query: path= or key=, plus workspace?. Walks the recorded chain and never guesses. | {workspace, subject, chain[], complete, note}. Each chain step carries when, who, what and a citation. note says plainly what is missing when the chain is incomplete. |
| GET | /api/links | bearer or session | What else in this space's knowledge points at one document. Query: path=, plus workspace?. Derived at read time from the serving projection, never a stored index, so it cannot go stale against the document it describes. | {workspace, path, links[{path, kind, title, excerpt, doc_class, how}], truncated}. how is the relation: targets, applies-to or source-process when the frontmatter names this document; process, skill or source-process when this is a skill and a page names it by its name; links when the prose does. truncated says the bounded candidate scan stopped before the end rather than presenting a clipped list as the whole. |
| GET | /api/page-history | bearer or session | A page's earlier versions, as page_history reads them. Query: path=, version? (one listed version), plus workspace?. Readable by whoever can read the page; an earlier version only when it declares a part of the space the reader can open. | Without version: {path, exists, versions[{commit, change, at, author, start_unknown, summary?, decision?, member?}], more}, newest first. With version: {path, exists, version, content, change, before_unknown, same_as_now}; content is the whole page at that version and change its line view. 503 {error, still_reading} while the space's history is being read for the first time. |
| POST | /api/page-history | teammate+ session, same-origin browser | Put an earlier version of a page back. Body: {workspace?, path, version}. Never writes the page: it opens an ordinary proposal carrying that version's whole text, on a run of the signed-in person's own, ruled through the usual yes. | {ok, proposed, decision_key, decision_url} when the proposal waits for a yes; {ok, same} when the page already reads as that version and nothing was proposed; otherwise {error, detail?} with 403 or 422 for a refusal a retry will not change, 503 with still_reading, or 409 for one worth retrying. |
| GET | /api/what-changed | bearer or session | Who changed which pages since a time, as what_changed reads them. Query: since? (an ISO date or date and time; a time with no zone is UTC; default 24 hours ago; at most 90 days ago, an earlier since is moved up for every reader alike), plus workspace?. Only pages the reader can read today. | {since, since_defaulted, since_capped?, until, people[{name, you, pages, flavour?}], total, rows, pages[{path, title, exists, change: added|changed|removed, at, changes, what, by[{name, you, changes, flavour?}], summary?} | {sync: true, name, you, flavour?, at, title, what, pages[{path, title, exists, change}]}], more, complete, gaps?}, newest first. total counts pages; rows counts the rows, where one agent's sync of its own home is one row. flavour is an agent's colour. gaps, when the answer may be short, lists older_history, too_large or too_many (more than 2000 changes to pages the reader can read). since_capped is true when since was moved up to 90 days ago. 400 {error} when since is not a real ISO date or date and time. 503 {error, still_reading} while the space's history is being read for the first time. |
| GET | /api/skills | bearer or session | Each skill the reader may open, as a person reads it: what it does, how often work follows it, how fresh it is, and its tags. An old processes/ page with a skills/<name>/SKILL.md of the same name is listed once, as the skill, and its runs, changes and lessons count for it. Query: path? for one skill and its timeline, plus workspace?. Skills, Decisions and Lessons pass the knowledge gate; runs pass the list_runs gate. | {workspace, weeks, skills[{path, name, title, description, state, origin: yours|starter (starter while a "Start with" example, its state example, in metadata or at the top of the page), tags[] (the skill's own metadata.tags, lower case), last_used{at, by}|null, weekly_runs[8, oldest first], runs_in_weeks, last_changed{at, title, by: decision|file}, lessons{waiting (pending Lessons naming it), absorbed}, runs_awaiting_decision, agents[{name, runs}] (who ran it in the weeks, most first, at most 5), author (who asked for the approved change that created it, a person or an agent; null when it came with the space)}], tags[{tag, count}] most used first, truncated, runs_truncated, last_used_truncated, decisions_truncated, lessons_truncated, authors_truncated}. With path=: {workspace, weeks, skill, waiting{lessons[{title, path, since}], runs[{who, since}]} (at most 10 each), decisions{items[{at, title, path}] (the 5 newest that changed it), total}, timeline[{type: run|change|lesson, at, who?, status?, title?, path?}] newest first and at most 30, timeline_truncated and the same five flags}; 404 when the reader cannot open that skill. Each *_truncated flag says a bounded read stopped early: counts are a floor, last_used may be unknown, and a change or lesson may be missing. |
| GET | /api/home-activity | bearer or session; agents Founder-only | Latest saved objects on Home. Query: workspace?, section=knowledge|agents, limit? (1–12), after? (knowledge only). Knowledge uses ordinary current knowledge access and complete projection freshness. Both publication-observation scope and current object scope are checked before pagination. Skills and legacy Process aliases appear once. First complete baseline and changed source bindings produce no backfilled dates. Agents reads active authorized machine membership and profile owner availability without Git, and says registration, never running. No new read authority. | {workspace, items[] {id, kind: skill|lesson|decision|agent, action: added|updated|recorded, title, occurred_at, timestamp_basis: publication_observed|registration, source: {path?, member?, version?, blob_sha?}, state: saved|pending|proposed|recorded}, checked_at, next_cursor, partial: false, has_saved_content? (knowledge), has_agents? (agents), coverage?}. has_saved_content checks current accessible eligible objects even when their initial baseline dates are unknown; has_agents checks active authorized machines even when their registration date is unavailable. An empty recent feed alone never proves a new space. Verified explicit titles only. Pending Lessons are saved awaiting incorporation; Decisions are recorded, never claimed applied. Knowledge dates are verified content changes observed at publication, never authored dates. A stale cursor returns 409 with cursor_stale; unavailable freshness or changed authorization returns no items. Cache disabled. |
| GET | /api/done | GitHub human session | Done for you: the schedule changes agents made on their own in this space, for its owner or a co-owner. Query: workspace? (needed when the person holds more than one). | {on, default_on, always_on, care_on, can_change, today, items[] {id, agent, action (add, pause, remove, default or always), line, at, undone}, standing?[] {id, line, at}}. standing is the owner's live standing yeses, sent to the owner only. care_on is the hourly page update, off until the owner turns it on. An id of ask-<key> is an ask the 7-day default or a standing yes approved. The last day, plus anything not undone from the last week, newest first, at most 20. today counts the last day's changes not undone. 401 with no session, 403 for anyone but the owner or a co-owner. |
| POST | /api/done/:id/undo | GitHub human session, same-origin browser | Undo one schedule change an agent made on its own. Writes the inverse with the same compare-and-swap, attributed to the signed-in owner; the owner only. | {ok, id, line}. 401 with no session, 403 from another origin or for a co-owner, 404 when this person may not see it, 409 when it was already undone, a later change to the same schedule is still in place (undo that first), or the file changed since. |
| POST | /api/done/setting | GitHub human session, same-origin browser | Turn one owner switch on or off for this space; the owner only. Each is off by default. Body: {on: true|false, setting?: just_done|default|always|care}. No setting means Just done; default is "If I don't answer in 7 days, go with the recommendation" and always offers Always after two yeses to the same change. care lets the hourly check add a ruling you already recorded onto the exact page that ruling covers, and it stays off until you turn it on. Query: workspace?. | {ok, on, setting}. 401 with no session, 403 from another origin or for anyone but the owner, 400 without a true or false or for any other setting. |
| POST | /api/done/ask-:key/undo | GitHub human session, same-origin browser | Undo an ask the 7-day default or a standing yes approved: a reverting ask with the inverse change, ruled yes by the signed-in owner and landed with the same per-file compare-and-swap. The owner only. | {ok, id, line}. 401 with no session, 403 from another origin or for a co-owner, 404 when this person may not see it, 409 when it was already undone, is still being carried out, or a file changed since. |
| POST | /api/done/standing/:id/revoke | GitHub human session, same-origin browser | Take back a standing yes. The owner only; it stops matching at once. Query: workspace?. | {ok, id}. 401 with no session, 403 from another origin or for anyone but the owner, 404 when there is no live standing yes with that id. |
| GET | /api/space/export | owner session | Take everything with you: the whole space as one ZIP of plain files. Query: workspace?. Read through the same freshness fence and owner check as an agent's home; a member session gets 403 and a machine bearer is refused with 403. Every row read is bound to this space. | A streamed application/zip attachment named <space>-mainmind.zip: knowledge/<path> for every page (agents/ included), files/<upload_id>/<filename> for each saved original, work.json with open and recent work (path, kind, status, title, body, asked_by, assignee, outcome, timestamps) and a README.md. Never a password, connected-account key, token or idempotency key. |
| GET | /api/space/github-copy | owner session | Copy to GitHub: whether this space keeps a copy in a GitHub repository, where, and how the last copy went. Query: workspace?, choices?: 1 also lists the private repositories the signed-in owner could pick. A member session or machine bearer gets 403. | {workspace, storage: mainmind|github, copy: {status: off|waiting|current|behind|connected, repository?, branch?, github_head?, copied_at?, error?, retry_at?}, choices?[] {repository, installation_id, repository_id}, install_url?, choices_error?}. No token. |
| POST | /api/space/github-copy | owner session | Turn Copy to GitHub on or off for a space that lives in Mainmind. Query: workspace?. Body: {repository: "owner/name"} to start, or {off: true} to stop. The repository must be private, pushable by the owner through the Mainmind GitHub App, and unused by any other space. The copy is one-way: Mainmind pushes its main as a fast-forward after every change, in the background, and never pulls from or forces GitHub. | {ok, copy} on start (the first copy runs in the background), or {ok, copy: {status: off}, left_on_github?} on stop; GitHub is left as it was. 403 for anyone but the owner, 409 for a refused repository or a space that started from GitHub. |
| GET | /api/team | bearer or session | Who is in one space. Query: workspace?, view?: roster. The roster view reads live membership and authenticated agent contact without waiting for canonical knowledge; context_pending is true and knowledge permissions are unverified. The full view adds freshness-checked Role and work context. Both reads have an eight-second deadline. A browser session is locked to spaces owned by its signed-in Founder. | {workspace, members[], available_scopes, scope_enforced, scope_source, scope_policy, policy_error}. scope_policy is the exact review snapshot for a browser access change; members carry slug, name, role, charter, knowledge scopes, status and activity timestamps; directory carries authenticated presence. The roster view omits knowledge scopes and sets policy_error until the full view verifies context. No credentials. |
| GET | /api/team/:slug/home | bearer or session | One agent's home for the Team screen: what it remembers, where it stopped, the apps it has used and its schedule. Founders only; anyone else gets 403. 404 for an unknown slug or a member that is not an agent. | {agent, name, home: present|missing|not_projected, instructions_summary, boundaries[], remembers[] {name, description, memory_kind, updated, from, sha}, stopped {date, app, summary, next[], open_questions[], unresolved_effects[]} or null, journal_count, apps[] {harness, first_seen, last_contact}, description, avatar {style, seed} or null, skills[] {name, description}, plugins[] {id}, routines[] {id, when, timezone, do, why, state, source: host|approved, last_run {slot, harness, state} or null}, source_commit}; a built-in agent adds builtin (librarian|toolsmith) and outcomes {versions[] {version, since, proposed, approved, rejected, waiting, sent_back, undone} newest version first, truncated}, how its proposals went under each version of its instructions: each proposal counts for the version stamped on it when it was filed (one filed before stamping counts for the version current on its day), and sent_back counts the ones the owner answered with Change it that are still open. source host was recorded from an app and is not an approval. No credentials. |
| GET | /api/team/questions | owner session | What every active agent is waiting on the person for, in one read, for Your call: the same open questions and checks as each agent's work read, from its latest stops and the answers on its page. Reads no work cards. At most 40 agents. Founders only; anyone else gets 403. | {agents[] {slug, needs_you[]} or {slug, error}, more}. more is true when agents past the limit were not read. No credentials. |
| GET | /api/team/:slug/work | owner session | One agent's work for its page: what it needs from the person, its checklist with who asked, and its routines. Founders only; anyone else gets 403. 404 for an unknown slug or a member that is not an active agent. | The agent's open questions, checklist and routines, with source_commit. No credentials. |
| POST | /api/team/:slug/answer | owner session | Answer what an agent is waiting on. Body: {question, answer, idempotency_key}. The answer is an ordinary comment on the agent's page, marked with the question. Founders only. | The recorded comment receipt, or a refusal. |
| POST | /api/team/:slug/forget | owner session | Forget one thing an agent remembers, from its page. Body: {name, expected_sha}, the memory's name and sha from /api/team/:slug/home. It is the same forget an agent sends when told "forget that": the memory is marked forgotten in the agent's own folder and no app reads it again; the change is the owner's own, re-checked when it lands. Founders only; anyone else gets 403. 404 for an unknown agent or memory; 409 when the memory changed since it was read, and nothing is forgotten. Forgetting one already forgotten answers ok. | {ok, name, readable} or {ok, name, already: true}; or {error} saying what was kept. No credentials. |
| POST | /api/team/invite | bearer or owner session | Invite a co-founder, teammate, or viewer from the owner app. Body: {name, role, charter?, knowledge_scopes?, scope_policy}; workspace is locked to the space on screen. Co-founder primary focus is optional and knowledge access is every live non-Founder scope; teammate and viewer carry a required charter and selected knowledge. An owner-session co-founder grant must match the exact policy snapshot the authenticated Founder reviewed. A machine bearer may invite only a teammate or viewer; its co-founder request is refused before mutation. A kind: machine body is refused with HTTP 403 before mutation: a new agent joins through its owner with register_agent. | The member and a browser Team URL, or HTTP 403 with confirmation_required, next_action and team_url for a machine-bearer co-founder request. No GitHub credential crosses this response. |
| GET | /api/team/requests | owner session | People who reach this space's repository on GitHub and have asked to be recorded here. Repository access is evidence the asker is a real collaborator, never authority: nothing is granted until the Founder answers. | {workspace, requests[]} with each request's GitHub login, the repository that proved the ask was real, an optional note, when it was asked, and member_status (null, or the roster's status for an account this space already recorded or revoked — such a request cannot be accepted, only declined). No codes and no credentials. |
| POST | /api/team/requests/:id/approve | owner session | Answer a waiting access request with an ordinary invitation. Body is the same as /api/team/invite — {role, charter?, knowledge_scopes?, scope_policy} — so the role, knowledge boundary and reviewed policy snapshot all come from the Founder's Team form, never from the request. The request records which invitation answered it. A kind: machine body is refused with HTTP 403 and the request stays open. | The new member, bound to the requester's GitHub login (bound_login, status active) so they connect with GitHub at once and no code changes hands; when that binding cannot complete, bound_login is null, bind_error says why and the invitation stays for its link. Plus request_settled. The answer is taken before the invitation is minted, so a second answer to the same request is refused with 409 and creates nothing. A request from an account this space already recorded or revoked is refused with 409 and the reason, whatever the queue showed. |
| POST | /api/team/requests/:id/decline | owner session | Close a waiting access request without inviting anybody. Nothing about their repository access on GitHub changes. | {ok, id, status}. A second answer to the same request changes nothing. |
| GET | /api/team/actions/:key | owner session | Review an unexpired team-access proposal queued by MCP. The signed-in Founder and space must match the proposal. | The proposed invite or revocation, requester and expiry. No credential. |
| POST | /api/team/actions/:key/confirm | owner session | Confirm an unexpired MCP team-access proposal. The access change, audit receipt and one-time action consumption commit atomically. | For an invite, the new member, seven-day invite expiry, and the one-time invite code the Team screen turns into a /join link, which grants nothing until the invited person signs in with GitHub; for a revocation, {ok, slug}. No MCP or machine bearer may call this route. |
| GET | /api/team/:slug/invite-code | owner session | Reveal one unused and unexpired invitation in the authenticated Team screen. Machine bearers and MCP cannot use this route. | {slug, name, invite_code, invite_expires_at}, only while status is invited and before the seven-day expiry; unavailable after use, cancellation, or expiry. |
| POST | /api/team/:slug/update | bearer or owner session | Update a non-Founder member's role, charter and knowledge access inside the space on screen, with the change and audit committed atomically. Body: {role: cofounder|teammate|viewer, charter?, knowledge_scopes?}. An agent that works from its owner's account takes its access from that person, so its body is {charter} alone: role or knowledge_scopes is refused, and the charter follows register_agent's rules. Co-founder focus is optional and access automatically follows every live non-Founder scope; teammate and viewer require both responsibility and selected knowledge. An authenticated owner session may submit the exact co-founder grant shown in Team; a machine-bearer co-founder request is refused before mutation. | The current credential-free member record, or HTTP 403 with confirmation_required, next_action and team_url for a machine-bearer co-founder request. Current connections use the live role, charter and scopes on their next operation; a host may need to refresh or reconnect before newly available tools appear. |
| POST | /api/team/:slug/revoke | bearer or owner session | Revoke a non-Founder teammate inside the space on screen and audit the access change. | {ok, slug}; future authorization and current tool calls are denied. The repository and other spaces are untouched. |
| GET | /oauth/amazon/login | the session that started Sign in with Amazon | The Login URI of the owner's app in Seller Central. Amazon opens it with amazon_callback_uri, amazon_state, selling_partner_id and state. The state must be unused, unexpired and started by this browser's session, and amazon_callback_uri must be https on a Seller Central host with no login or port. | 302 to amazon_callback_uri with redirect_uri, amazon_state and state; otherwise a plain page that echoes nothing. |
| GET | /oauth/amazon/callback | the session that started Sign in with Amazon | The Redirect URI of the owner's app in Seller Central. Amazon opens it with state, selling_partner_id and spapi_oauth_code (or error). The state is spent exactly once and its sealed values emptied in the same write; the code is exchanged (10-second timeout) at Amazon's token endpoint with the sealed client ID and secret, and the refresh token is stored as the built-in Amazon account, placed when none was stored at the start and otherwise replaced from the generation recorded at the start (an account changed meanwhile is left as it is, with a page saying to start again); a replacement forgets cached access and restricted-data tokens. | An HTML page: Amazon is connected, or what went wrong and what to do. Never the code, a token or a secret. |
| GET | /oauth/zoho/connect | GitHub session of an owner or co-owner | Sign in with Zoho. One button, no redirect. Query alias names a Connection. provider is zoho-desk or zoho-inventory. workspace chooses the space when the person holds more than one. A scope query is ignored. | An HTML page with Sign in with Zoho, a list of spaces, or a fixed page when sign-in is not available. No secret is rendered. |
| POST | /oauth/zoho/connect | the session that opened Sign in with Zoho | Starts Sign in with Zoho. Accepted only from a form this origin served. Parks a single-use state for fifteen minutes, bound to this login, and redirects to the Zoho accounts host for that data centre. The scope comes from Mainmind's catalog. | 302 to Zoho. 503 when the Zoho client is not configured, and nothing is stored. |
| GET | /oauth/zoho/callback | the session that started Sign in with Zoho | Zoho returns the browser here with state and code, or error. The state is spent once and its row emptied in the same write. Mainmind exchanges the code with redirect_uri and stores the refresh token on the Connection or built-in account, placed when none was stored and otherwise replaced from the generation recorded at the start. | An HTML page: Zoho is connected, or what went wrong. Never the code, a token or a secret. |
| GET | /place/:key | GitHub human session | The placement page for a Connection an agent installed without its values. Authentication happens before the key is looked up; the key resolves only for a live Founder of the pending Connection's space, or a co-founder there, and only while unexpired and unused. The page shows who asked when an actor name was recorded, the alias, the driver and the exact origin and header the values will be sent to, then one write-only input per field. Signed out: the same sign-in page the decision link shows. Anyone else, and any used, expired or invented key: one 404 that discloses nothing. | An HTML page with a nonce-bound progress script and form-action 'self'. No value is ever rendered. |
| POST | /place/:key | GitHub human session | Place the values. Accepted only from a form this origin served (Origin must equal the page origin), for the same person the GET admits. The values pass the same refusal and sealing path as an install with values; one conditional update flips the Connection active, mints its credential generation, records who placed and burns the key. A refusal re-renders the form without echoing anything typed. | An HTML page naming the alias as active, with no form. A spent or expired key answers 404. |
| GET | /grant/:key | GitHub human session | A one-hour link an owner or co-owner opens to sign in with Zoho. Authentication happens before the key is looked up. Signed out: the same sign-in page a decision link shows. Anyone else, and any used, expired or invented key: one 404 that discloses nothing. The page is one button. A built-in account that has no data centre yet also asks for that and the Organization ID. | An HTML page. No code or secret is rendered. |
| POST | /grant/:key | GitHub human session | Redeem the Zoho code. Accepted only from a form this origin served. Mainmind exchanges the code, checks it with Zoho, and only then replaces the saved login. A refusal keeps the previous login and leaves the link usable. Success burns the link. | An HTML page saying Zoho accepted the code. A spent, expired or invented key answers 404. A failed check re-renders the form without echoing the code. |
| POST | /api/tool-connections/seal | single-use key | Seal a pending or active Connection. The URL has no key. Send the key in the header X-Mainmind-Seal-Key only. A missing or invalid header is 404 before the body is read, whatever the content type. There is no browser session, because a shell posts the values from the environment. A Founder or co-founder obtains the key from install_provider_connection with request_seal_link true, or from replace_provider_connection_credential with request_reseal_link true, the same people who use Connect and Replace values on Accounts. The body is one JSON object of the declared field names, content-type application/json, at most 8192 bytes. A failed check leaves the link usable and does not echo the values. Success burns the link. Any other method, and any used, expired or invented key, answers 404. | {ok, alias, kind, credential_generation, fields}. fields maps each name the post sent to {bytes, fingerprint}: the UTF-8 length and the SHA-256 hex of the string that was stored. A value kept from the previous seal is not named. No values. 400 names a bad object or a field the profile does not declare, without the values. 403 when the member who asked is no longer an owner or co-owner, or the workspace has no active owner; the link stays unused. 409 when the generation moved or the pending row is no longer that row. 415 when the body is not JSON. |
| POST | /api/gateway-secrets | owner or co-owner | Save Zoho Expense. The body is one JSON object: alias, preset zoho-expense, region, optional meta.organization_id, and fields. Mainmind seals the fields, checks the seal, then asks that region's accounts host and reads the organizations list. Anything other than HTTP 200 stores nothing. The answer lists each field's byte length and SHA-256 and never a value. | {alias, preset, region, generation, fields, verify}. verify is the organizations address, HTTP 200 and the time. 422 names the host and the provider status and stores nothing. 500 when the seal does not match, and stores nothing. 403 for anyone who is not an owner or co-owner. |
| POST | /api/gateway-secrets/seal | single-use key | Save Zoho Expense from a shell. The URL has no key. Send the key in the header X-Mainmind-Seal-Key only. A missing or invalid header is 404 before the body is read. An owner or co-owner obtains the key from issue_gateway_seal. The body is one JSON object of client_id, client_secret and refresh_token. The alias, preset and region are the ones on the link, so the post cannot retarget the host. A failed check leaves the link usable and does not echo the values. Success burns the link. The answer lists each field's byte length and SHA-256 and never a value. | {alias, preset, region, generation, fields, verify}. 404 for a missing, used, expired or invented key, before the body is read. 422 when Zoho does not answer 200, and the link stays usable. 403 when the member who asked is no longer an owner or co-owner. |
| POST | /reseal/:key | single-use key | Reseal an api-key or sign-in Connection. The key is the capability: there is no browser session, because a shell posts the values from the environment. A Founder or co-founder obtains the key from replace_provider_connection_credential with request_reseal_link true. Kept so a key already issued with the key in the URL still works. A key issued for the header is refused here, and that link stays usable. New links use POST /api/tool-connections/seal, with the key in the header X-Mainmind-Seal-Key. The body is one JSON object of the declared field names, content-type application/json, at most 8192 bytes. A failed check leaves the link usable and does not echo the values. Success burns the link. Any other method, and any used, expired or invented key, answers 404. | {ok, alias, kind, credential_generation, fields}. fields maps each name the post sent to {bytes, fingerprint}: the UTF-8 length and the SHA-256 hex of the string that was stored. A value kept from the previous seal is not named. No values. 400 names a bad object or a field the profile does not declare, without the values. 403 when the member who asked is no longer an owner or co-owner, or the workspace has no active owner; the link stays unused. 409 when the generation moved. 415 when the body is not JSON. 503 when that membership or the installed profile cannot be checked. |
| GET | /api/source-context | owner/member session or source-only capability | Read bounded extracted context for record, with optional query and cursor. export=1 returns the exact authorized manifest and original fingerprint for offline export. | Source identity, extraction coverage, located excerpts and next_cursor; pending/failed extraction never claims completion. |
| GET | /api/source-files | Founder session or source-only capability | Read the bounded raw-source upload contract for the space on screen: accepted document/image extensions, maximum bytes, source-document types, and the live access-scope vocabulary. Returns no file bytes. | {max_bytes, accepted_extensions[], doc_types[], available_scopes[]}. |
| POST | /api/source-files | Founder session or source-only capability | Save a multipart original or JSON begin/append/finalize operation. Folder copies use import_begin with a reviewed plan, import_file with import_id/path, import_status and import_finish. Plans bind up to 100 files/100 MiB to the current space, member instance and scope. Existing uploads retain originals; a saved inventory maps relative paths to their source records. No synchronization or instruction execution occurs. | Saved multipart uploads return HTTP 201; JSON operations return HTTP 200. File receipts include upload_id, saved, path, resource_uri, sha256, size, commit, storage and extraction. Import status includes plan, files, originals_saved, total, extraction_ready and inventory. An uncertain save remains distinct; retries retain its operation. |
| GET | /api/source-file | owner/member session or source-only capability | Read one retained original by its source-doc register path. Query: workspace?, record. The register is read through the normal freshness and live knowledge-scope gate, then the private R2 receipt is matched to that exact manifest and original bytes are fingerprint-checked. Legacy in-Git originals retain their exact-commit GitHub reader. | The original bytes with registered MIME type, safe inline/attachment disposition, no-store caching, nosniff, sandbox, and a SHA-256 ETag; 304 only for that exact fingerprint. |