# A role's version history + audit trail Source: https://docs.tai42.ai/api-reference/access-control/a-roles-version-history-+-audit-trail /openapi.json get /api/auth/roles/{name}/versions The role's append-only version history plus its who/when/before→after audit trail. Admin-only. A store-less deployment keeps no history, so the read is an empty pair. # Add a URL to a scope Source: https://docs.tai42.ai/api-reference/access-control/add-a-url-to-a-scope /openapi.json post /api/auth/scopes Map ``url`` to ``scope_id`` (optionally with a dynamic match ``pattern``). # Add/remove scopes on an api key Source: https://docs.tai42.ai/api-reference/access-control/addremove-scopes-on-an-api-key /openapi.json post /api/auth/api-keys/{user_id}/scopes Add and/or remove named scopes on a key's stored scope set without replacing the whole set. The new set keeps the stored order, drops the removed scopes, then appends the additions in the given order. A plain read-merge-write with NO new locking: two simultaneous edits of one key can lose one (accepted for this surface). # Create a one-time claim link for an API key Source: https://docs.tai42.ai/api-reference/access-control/create-a-one-time-claim-link-for-an-api-key /openapi.json post /api/auth/claim-links Mint a one-time claim link that carries ``api_key`` to another device (the QR onboarding leg). The submitted key is resolved through the gate's own verifier chain and the caller must own it (or be admin) per the module's ownership rule; the response returns the claim token ONCE plus a fragment-carrier path on the declared hosted-UI login page (``/login#claim=`` by default) and an expiry. Accepted oracle (deliberate, not an oversight): an unresolvable key answers 400 and a valid-but-not-yours key answers 403, so an authenticated caller can tell a live key from garbage. This adds NO capability the ``/api/auth/me`` carve-out does not already grant a caller holding a candidate key. The uniform-404 no-oracle rule governs the unauthenticated EXCHANGE surface, never this authed creation. # Create a principal Source: https://docs.tai42.ai/api-reference/access-control/create-a-principal /openapi.json post /api/auth/principals Create a ``human`` or ``service`` principal and apply its role (admin only). The door mints a ``user_id`` when absent. A duplicate id is a 409; an unknown role is a 400. ``created_by`` records the admin who created it. # Create a role Source: https://docs.tai42.ai/api-reference/access-control/create-a-role /openapi.json post /api/auth/roles Create an operator-authored role. Admin-only; validates the grant map + base tier before persist; 409 on a name collision. Bumps the policy version and audits. # Create an api key Source: https://docs.tai42.ai/api-reference/access-control/create-an-api-key /openapi.json post /api/auth/api-keys Provision a key, returning ``{"api_key", "key_fingerprint"}``. The raw ``sk-…`` ``api_key`` is surfaced ONCE; ``key_fingerprint`` is the key's immutable per-mint identity a caller binds a hook against so the binding survives only this exact mint. # Delete a principal Source: https://docs.tai42.ai/api-reference/access-control/delete-a-principal /openapi.json delete /api/auth/principals/{user_id} Delete a principal, revoking every key it owns plus its policy row (admin only). Refused (409) when a login-owning accounts provider keeps a login record for the principal — that provider's member actions on the Members page remove the principal and the record together, so the record never outlives the principal. A principal no provider claims (service, hand-provisioned OIDC human, keys-only owner) is deleted here; deleting the last enabled admin principal is refused (409) by the last-admin guard. A missing principal is a 404. # Delete a role Source: https://docs.tai42.ai/api-reference/access-control/delete-a-role /openapi.json delete /api/auth/roles/{name} Delete a role. Admin-only; the reserved ``admin`` role is undeletable; a role still assigned to any principal (its LIVE pointer held by any policy) is rejected loudly so a holder can never be orphaned. Bumps the version and audits. # Delete a scope Source: https://docs.tai42.ai/api-reference/access-control/delete-a-scope /openapi.json delete /api/auth/scopes/{scope_id} Delete a scope, cascading it out of every referencing key. An unknown scope (no urls) is a loud 404. # Edit a role's grant map Source: https://docs.tai42.ai/api-reference/access-control/edit-a-roles-grant-map /openapi.json put /api/auth/roles/{name} Edit a role's per-tag grant map + description (the base-tier jq is seed-fixed, not editable here). Admin-only; guards the reserved ``admin`` role and the block-downgrade of any allow_all role. Both inputs are omit-means-keep — an absent ``grants``/``description`` preserves the stored value, so a description-only edit never wipes the grant map. Validates a supplied grant map before persist; LIVE — the edit changes every holder's reach on their next request via the policy-version bump. Audits. # Edit an api key Source: https://docs.tai42.ai/api-reference/access-control/edit-an-api-key /openapi.json put /api/auth/api-keys/{user_id} A PATCH-style partial edit: only the fields present in ``updates`` are overwritten. A field absent is preserved at its stored value, so saving a description or scope change never silently drops an authorization ``condition`` or ``policy_data`` gate. ``updates`` is the sparse set of present fields (a single dict rather than flattened params, so "field absent" stays distinct from "field is ``null``" — the partial-edit semantics a flat signature cannot express). # Initialize the deployment (owner + first key) Source: https://docs.tai42.ai/api-reference/access-control/initialize-the-deployment-owner-+-first-key /openapi.json post /api/setup Initialize the deployment once behind the secure-by-default setup-token gate. Only meaningful on a serviceable access-controlled install: it refuses loudly with a 501 when the door cannot initialize — access control off, no configured key-minting identity provider, or the access-control Redis unset. The per-IP backoff is consulted BEFORE the token, so a wrong-token flood escalates a lockout that turns further attempts away without ever comparing; a wrong/absent token is a generic 403 (no oracle for the initialized state). Under one mint mutex: 409 "Already initialized" when ANY principal exists; else the owner principal (``kind=human``) is created, granted the admin role, its first key (owned by the owner) is minted and returned ONCE, and — when a configured accounts provider can attach a login and the request carries one — the owner's login is attached (a password set now, or an invite whose one-time link is returned). A failure past the owner create is compensated so setup stays retriable; a correctable login credential is compensated then mapped to the caller (a too-short password → 400, a login/email collision → 409) rather than surfaced as a 500. # Invoke a member action Source: https://docs.tai42.ai/api-reference/access-control/invoke-a-member-action /openapi.json post /api/auth/member-actions/invoke Invoke one declared member action by its opaque key (admin only). The key decodes to ``(provider, action_id)`` and the optional handle to the ``(provider, target)`` the action acts on; a malformed token or a key/handle naming different providers is a loud :class:`BadRequestError`. The per-action ``input`` is validated by ``input_model.model_validate`` (ordinary pydantic — a declared-field type error is a 422, an unknown key is ignored). The provider's typed failure propagates and the platform reads no field of the opaque result. # List a user's policy version history Source: https://docs.tai42.ai/api-reference/access-control/list-a-users-policy-version-history /openapi.json get /api/auth/api-keys/{user_id}/policy/versions The user's append-only policy version history from the durable PG store. Each row is flagged ``is_current`` against the active pointer. Secret-adjacent (a version body carries the raw condition) and admin-only: a non-admin caller is denied 403 so it can never read another user's policy history. 404 when the user has no policy history. # List all scopes Source: https://docs.tai42.ai/api-reference/access-control/list-all-scopes /openapi.json get /api/auth/scopes Every non-public route mapping as ``{url: scope_id}``. # List api-key token payloads Source: https://docs.tai42.ai/api-reference/access-control/list-api-key-token-payloads /openapi.json get /api/auth/tokens-payload Every provisioned key's identity + policy (NEVER key material). Non-admin callers see ONLY the keys they own (management/listing owner home); admin sees every key. # List member actions Source: https://docs.tai42.ai/api-reference/access-control/list-member-actions /openapi.json get /api/auth/member-actions The deployment-wide catalog of declared member actions (admin only). Each registered accounts provider's :meth:`member_actions` declarations are serialized into opaque-keyed :class:`MemberActionDescriptor` rows, the label resolved through the resource manager and the input/result schemas rendered generically. The platform reads no action field. # List members and invites Source: https://docs.tai42.ai/api-reference/access-control/list-members-and-invites /openapi.json get /api/auth/members Aggregate every registered accounts provider's people and invitations (admin only). Each provider's :meth:`~tai42_contract.accounts.provider.AccountsProvider.list_members` supplies its own members and invites; the aggregate wraps them into wire rows in provider name order, minting each row's opaque handle and action keys and joining each member principal's ``disabled`` state (the principal store, read once per call) and the role it holds (one ``principal_roles`` read for every member principal). A member row's ``disabled`` is derived True only when EVERY principal is disabled; its ``role`` is the one role every principal holds, else ``None``. Provider errors propagate (loud, never a silently empty listing). # List principals Source: https://docs.tai42.ai/api-reference/access-control/list-principals /openapi.json get /api/auth/principals Every principal (admin only). # List public-pinned routes Source: https://docs.tai42.ai/api-reference/access-control/list-public-pinned-routes /openapi.json get /api/auth/public-routes Every route pinned to the public marker. # List roles Source: https://docs.tai42.ai/api-reference/access-control/list-roles /openapi.json get /api/auth/roles The seeded/operator-authored roles as full ``RoleDefinition``-shaped bodies. Each body is ``{name, description, scopes, condition, base_tier, allow_all, grants}`` — the Members page's role action and the Studio Roles page read this. A store-less deployment (no versioned store configured) has no roles — the seed step is skipped at boot — so the read is skipped and the list is empty. Admin-only: a listing exposes every role's raw base-tier jq condition, so a non-admin caller is denied 403. This op-level check is defense in depth behind the ``secret`` route action-class that already fences the route. # List the app's HTTP routes and their scope mappings Source: https://docs.tai42.ai/api-reference/access-control/list-the-apps-http-routes-and-their-scope-mappings /openapi.json get /api/auth/routes Enumerate the app's own HTTP routes with each route's current scope mapping. Covers the mapper's route picker and its "unassigned routes" bucket (the ``mapped: null`` entries). ``routes`` is the app's live route table (the route-adapter extractor hands the operation ``request.app.routes`` so it stays request-free). One entry per :class:`starlette.routing.Route`, sorted by ``path``. ``Mount`` entries (the sub-MCP mount, the MCP mount) are EXCLUDED — a mount is not a bindable url and the sub-MCP surface has its own door. The exclusion is an explicit ``isinstance`` filter: any route-table entry that is neither a ``Route`` nor a ``Mount`` raises loudly (a new Starlette routing type must be classified here, never silently dropped). ``methods`` is the route's method set sorted with ``HEAD`` removed (Starlette auto-adds it to every GET route — noise for the mapper). ``mapped`` is the url's value from ``get_all_route_mappings`` looked up by the EXACT path string — a scope id, the public marker for a public pin, or ``null`` when the path has no mapping. Exact-key lookup only: this door does not attempt dynamic-pattern matching. ``declared_public`` is ``True`` for a route public by its own declaration: it answers everyone whatever scope it is mapped to (the access check's declared-public tier), so the scope writer refuses to map it. Each entry is JOINED with its :class:`RouteMetadata` (from ``route_registry.load_all_routes()``, a separate collection) to also carry the route's feature ``tags`` + ``summary`` + authorization ``action`` — the data the Studio Roles page groups the per-tag tri-state by and marks the ``fenced``/``secret`` (admin-only) routes it must never offer a grant for. The join is keyed on the route TEMPLATE + its method set with ``HEAD`` stripped on BOTH sides (Starlette auto-adds ``HEAD`` to a GET), so the two collections compare like-for-like; a registered route whose method set fails to join is a loud STOP (a normalization drift), never a silently dropped row. # Log out the current session Source: https://docs.tai42.ai/api-reference/access-control/log-out-the-current-session /openapi.json post /api/auth/logout Revoke the caller's session by fanning ``revoke_session`` out over every candidate and provider. Fans out over every presented credential candidate and every registered accounts provider. Iterating all candidates is required: a client may present a stale value in ``Authorization`` alongside its live session in ``X-Api-Key``, so checking only the first would leave that session alive. The FIRST ``True`` → ``{"revoked": true}``. Only when EVERY candidate against EVERY provider returns ``False`` → 404 (an ``sk-`` API key cannot "log out" — loud, not a silent no-op), with a server-side log naming the caller so probing/replay stays visible. Provider errors propagate (fail closed). # Pin a route public Source: https://docs.tai42.ai/api-reference/access-control/pin-a-route-public /openapi.json post /api/auth/public-routes Pin ``url`` public (optionally with a dynamic match ``pattern``). # Remove a URL from all scopes Source: https://docs.tai42.ai/api-reference/access-control/remove-a-url-from-all-scopes /openapi.json delete /api/auth/scopes/urls Unmap ``url`` from every scope that references it. A url that was never mapped is a loud 404 (a typo, not a silent success). # Report key-mint capabilities Source: https://docs.tai42.ai/api-reference/access-control/report-key-mint-capabilities /openapi.json get /api/auth/capabilities Whether any configured identity provider can MINT keys, per provider. Lets the Studio disable mint UI with a clear message on a validator-only deployment instead of surfacing a raw error at mint time. # Revoke an api key Source: https://docs.tai42.ai/api-reference/access-control/revoke-an-api-key /openapi.json delete /api/auth/api-keys/{user_id} Revoke a key (immediate: next request fails to auth). Deletes the key record, its enforced policy row, and its live context; the user's ``ac_policy`` version history is deliberately NOT touched (it belongs to the identity, so a key later re-created for the same ``user_id`` resumes that history). No version bump here, unlike every other mutation on this surface: revocation's cache-buster is atomic with the policy-row delete inside :func:`~tai42_skeleton.access_control.management.revoke_api_key`, so no fault in the steps behind it can leave the revoked key's authority live in a warm cache slot. # Roll a policy back to a version Source: https://docs.tai42.ai/api-reference/access-control/roll-a-policy-back-to-a-version /openapi.json post /api/auth/api-keys/{user_id}/policy/rollback Re-point the enforced policy to a prior version. Store-first: the target version body is read from the history, written to the enforced store (the authority) FIRST — the writer invalidates the policy cache — then the durable history pointer is advanced. Admin-only: a non-admin caller is denied 403 so it can never roll back another user's (or its own) enforced policy. 404 if the version is absent or the user has no live key. # Roll a role back to a version Source: https://docs.tai42.ai/api-reference/access-control/roll-a-role-back-to-a-version /openapi.json post /api/auth/roles/{name}/rollback Re-point a role's active version to a prior one (LIVE — holders follow on their next request). Admin-only; the reserved ``admin`` role has no editable history. Bumps the version and audits. # Set/remove single tag grants on a role Source: https://docs.tai42.ai/api-reference/access-control/setremove-single-tag-grants-on-a-role /openapi.json post /api/auth/roles/{name}/grants Set (upsert) and/or remove single tag grants on a role WITHOUT replacing the whole map. Admin-only; guards the reserved ``admin`` role and the block-downgrade of any allow_all role. ``upsert`` overwrites a tag's level (the one sanctioned overwrite); a ``remove`` tag absent from the stored map is a loud 404. Validates the merged map before persist; LIVE via the policy-version bump. Audits as ``edit``. # The caller's capability projection Source: https://docs.tai42.ai/api-reference/access-control/the-callers-capability-projection /openapi.json get /api/auth/me The authenticated caller's derived capability projection. The concrete routes, dynamic patterns, sub-MCP mounts, tools, and agents it can reach right now (derived, never stored). ``user_id``/``effective_scopes``/``claims`` are the caller's OWN identity, derived at the HTTP edge from the authenticated request — never caller-supplied. This is ``caller_context=True`` (tier-1, never projectable): as an MCP tool a caller would supply those identity params itself and read ANY principal's projection, so the HTTP route (``/api/auth/me``, whose extractor derives them from ``request.user``) is the only surface. With the gate OFF there is no identity to project (the edge passes ``user_id=None``), so a synthetic TOTAL projection is returned; otherwise the projection is built through the REAL enforcer so it can never advertise a door the gate would deny. Any infrastructure error propagates per the projection's failure doctrine. # Unpin a public route Source: https://docs.tai42.ai/api-reference/access-control/unpin-a-public-route /openapi.json delete /api/auth/public-routes Unpin a public ``url``; a url that is absent or scope-mapped is a loud 404. # Update a principal Source: https://docs.tai42.ai/api-reference/access-control/update-a-principal /openapi.json put /api/auth/principals/{user_id} Update a principal's display name and/or disabled state (admin only). Both fields are omit-means-keep. A ``disabled`` change is refused (409) when a login-attaching accounts provider holds the principal's credential — its disabled state lives with that provider and changes through its member actions on the Members page — and otherwise flips both homes through the guarded disabled writer (a principal whose login record a provider keeps without holding its credential is disabled here). Disabling the last enabled admin principal is refused (409) by the last-admin guard. A display-name edit is accepted for any kind. Returns the updated principal; a missing principal is a 404. # Validate a jq policy condition Source: https://docs.tai42.ai/api-reference/access-control/validate-a-jq-policy-condition /openapi.json post /api/auth/validate-condition Fail-closed guard: compile — and optionally sample-evaluate — a jq policy ``condition``, unpersisted. A syntactically broken condition raises at enforcement and DENIES the key (a lock-out), so authoring flows validate here before saving. The condition is rendered through the SAME ``render_condition`` enforcement uses, compiled with ``get_compiled_jq``, and — when a ``sample_context`` is supplied — decided by the SAME ``PolicyEnforcer.evaluate`` against a ``JqAuthContext``-shaped sample, so the validator's verdict is enforcement's by construction. This ONLY compiles/evaluates; it never writes any store. Returns ``{"ok": true, "result": }`` (``result`` is ``null`` when no sample was evaluated). An AUTHOR error is a loud ``BadRequestError`` (400); a server-side fault (an unconfigured resource manager, a redis/storage outage rendering a stored condition ``id``) is NOT an author error and propagates as a loud 500. # List every registered agent Source: https://docs.tai42.ai/api-reference/agents/list-every-registered-agent /openapi.json get /api/agents List every registered agent with its schema and spec-runnable marker, plus a total. # List the spec-runnable (authorable) agents Source: https://docs.tai42.ai/api-reference/agents/list-the-spec-runnable-authorable-agents /openapi.json get /api/agents/spec-runnable Only the authorable agents (``spec_runnable`` True) — the compose UI's base-agent picker. Filters on the marker, never on a known agent name; an empty list means no authoring is possible. # Stream a run of an agent Source: https://docs.tai42.ai/api-reference/agents/stream-a-run-of-an-agent /openapi.json post /api/agents/{name}/runs Stream a run of the named agent as SSE; 404 for an unknown agent, retriable while a reload holds the gate. # Stream a run of an authored agent Source: https://docs.tai42.ai/api-reference/agents/stream-a-run-of-an-authored-agent /openapi.json post /api/agents/authored/{name}/runs Stream a run of an authored agent — a preset over an agent's run tool whose baked fields it honors. The baked spec is read from the ``PresetManager`` in-memory map (the single source of truth for every registered preset — versioning never gates streamability). The request body supplies ONLY the remaining, non-baked ``ToolInput`` fields; naming a baked field is a loud 400 rather than a silent override. The baked fixed kwargs plus the request fields are combined at the FIELD level, validated against the agent's ``ToolInput``, and mapped through ``from_tool_input`` before ``astream`` — never a raw splat that would bypass validation and the field mapping. # Get the backend identity Source: https://docs.tai42.ai/api-reference/backend/get-the-backend-identity /openapi.json get /api/backend The backend identity — whether one is registered and, if so, its class and module. # List the worker fleet Source: https://docs.tai42.ai/api-reference/backend/list-the-worker-fleet /openapi.json get /api/fleet/workers The fleet listing — one row per live worker presence key, each with a server-computed ``stale`` flag. # Soft-restart the worker fleet Source: https://docs.tai42.ai/api-reference/backend/soft-restart-the-worker-fleet /openapi.json post /api/fleet/reload-config Soft-restart the worker fleet, optionally restricted to the named ``targets``. Called over MCP on a worker the reload applies to (no ``targets``, or ``targets`` naming that worker), the reload answers on the session that called it and then retires that session with the generation it ran on: a later call on the same session fails (streamable HTTP answers it 404, and an SSE session's stream closes), so the client starts a new session to go on. When ``targets`` leave that worker out, nothing reloads there and the session keeps working. Each worker's payload is ``{status, env_keys, awaiting_respawn}``: ``awaiting_respawn`` names the recycle-class keys whose value now differs from the one that worker bound (at boot, or when the plugin that declares them was loaded) — the soft restart carries every hot key in place, and those keys take effect only when the worker is respawned through the deployment's supervisor (on a bare deployment, a server restart). This operation ERROR-logs one line naming every such worker and its keys. # Export the named backup sections Source: https://docs.tai42.ai/api-reference/backup/export-the-named-backup-sections /openapi.json post /api/backup/export Export the requested backup ``sections`` as a downloadable JSON document. A failed section exporter is recorded under ``errors`` rather than aborting the export; a malformed request body or an unknown section name is a loud 400. # Import a backup document Source: https://docs.tai42.ai/api-reference/backup/import-a-backup-document /openapi.json post /api/backup/import Import each SELECTED section of ``document`` in registration order and return a per-section report. ``mode`` is the per-record policy (``skip`` or ``overwrite``). A ``version`` other than 1 raises ``BadRequestError``; an unknown, absent, or failing section carries its error in the report and makes ``ok`` false. A section whose restore rolled a worker recycle (an env section changing a recycle-class key on a supervised deployment) carries the roll in its ``recycle``; when that roll converged, this worker recycles itself after replying. # List backup sections Source: https://docs.tai42.ai/api-reference/backup/list-backup-sections /openapi.json get /api/backup/sections List the registered backup sections as ``{name, secret}`` entries. # List registered channels Source: https://docs.tai42.ai/api-reference/channels/list-registered-channels /openapi.json get /api/channels The registered channel names ``ask(channel=...)`` can resolve. # Sweep expired checkpoint threads Source: https://docs.tai42.ai/api-reference/checkpoints/sweep-expired-checkpoint-threads /openapi.json post /api/checkpoints/sweep Delete the checkpoint threads of the deployment's store that are past their retention horizon. A finished thread (marked by its owner) goes at the due time stored with its mark; on a store whose waiting horizon is swept any thread goes its waiting retention (declared by its owner, else ``checkpoint_retention_waiting_minutes``) after its newest checkpoint (a store that expires it by key TTL, or keeps it for the process, is reported in ``skipped``). A thread a registered live-thread filter claims is spared and listed. ``waiting_minutes`` and ``finished_minutes`` in the result are the platform's values: the default and ceiling of every thread's retention. # Apply a settings profile — replace the stored env and reload the fleet Source: https://docs.tai42.ai/api-reference/config/apply-a-settings-profile-—-replace-the-stored-env-and-reload-the-fleet /openapi.json post /api/config/profiles/{name}/apply Apply the profile's active env as the WHOLE stored env band and reload the fleet. The profile's ``secret_keys`` are written as the stored secret marks in the same replace. A key the profile omits is deleted, save the carried deployment X band; build+swap a fresh serving epoch under it, persist env-write-LAST, broadcast the reload, and recycle the fleet for recycle-class diffs. NO request body. The response is the dedicated ``profileApplyResponse`` ``{hot, recycle:[{name, kind, status, generation_before, detail}], fresh:[{name, kind, generation}], refused:[], recycle_stopped:{kind, name, detail} | null, awaiting_respawn:[{name, keys}], fanout}`` — key names + worker identities only, never env values. ``recycle`` is one line per sibling the roll reached (plus the applier's own deferred self-exit line when it is armed). A ``recycled`` line is confirmed on reality; the roll stops at the first line that is not: ``timed-out`` (its old life did not go, fresh capacity did not boot, or it never returned to ready, within the step budget) or ``failed`` (the worker refused the op, or the bus could not be reached while it was in hand), its ``detail`` naming why — or when the bus could not be read before a target was reached. ``recycle_stopped`` names where the roll stopped and why (``null`` when it ran to its end; a ``null`` name when no target was in hand). Later siblings and the applier's own recycle are then not attempted, and the call still answers with the report: the env is persisted and the fleet reloaded. ``fresh`` is the per-kind new ready lives observed since the pre-apply snapshot, capacity evidence never claimed as any target's successor. ``refused`` is ``[]``: a refusal (X-band key, dangling ``!ENV``, a recycle-class diff the deployment shape cannot carry) aborts upfront with a loud 400 naming the key, before anything is snapshotted, built, or persisted. ``awaiting_respawn`` names the workers (name + keys) still awaiting a respawn after the roll — empty when it converged. When the diff carries serve-affecting recycle keys and the roll converged, the applier's OWN recycle is armed as a post-reply graceful self-exit — after the HTTP body flushes, or once a projected tool call (MCP, ``tai tools run``, an agent, a preset, a schedule, a hook) returned — and its supervisor respawns it on the new env. 404 for an absent name. Called over MCP, the reload answers on the session that called it and then retires that session with the generation it ran on: a later call on the same session fails (streamable HTTP answers it 404, and an SSE session's stream closes), so the client starts a new session to go on. # Create or update a settings profile Source: https://docs.tai42.ai/api-reference/config/create-or-update-a-settings-profile /openapi.json put /api/config/profiles/{name} Create the profile, or append a new version when it exists (whole-body replace). Returns ``{ok: true, version}`` — NEVER the stored body, so a secret never re-emits on the write path. A reserved ``@``-prefixed name is a loud 400. The profile's DECLARED env is run through the shared boundary validator (``ConfigService._validate_replace``): a payload carrying an X-band key or leaving a manifest ``!ENV`` marker dangling is refused at save time, naming the key. # Delete a settings profile Source: https://docs.tai42.ai/api-reference/config/delete-a-settings-profile /openapi.json delete /api/config/profiles/{name} Soft-delete the profile, keeping its version history (audit). 404 for an absent name. # Diff a settings profile against the stored env Source: https://docs.tai42.ai/api-reference/config/diff-a-settings-profile-against-the-stored-env /openapi.json post /api/config/profiles/{name}/diff The saved profile's env vs the CURRENT stored env, with REAL values (the UI masks) — a preview, not a report. ``added`` / ``removed`` are key names, ``changed`` is ``[{key, old, new}]``. ``recycle_keys`` are the diff keys whose registry ``reload_class`` is ``recycle``; ``refused_keys`` are the diff keys the recycle policy refuses upfront on this deployment shape (``recycle_policy.capability_report().refused_keys``), named. 404 for an absent name. # Get a settings profile Source: https://docs.tai42.ai/api-reference/config/get-a-settings-profile /openapi.json get /api/config/profiles/{name} The profile's active body — ``{description, env, secret_keys}`` with REAL env values. This authed ``secret``-fenced door round-trips values through the editor; masking is display-side only, never on the wire. 404 for an absent name. # Get a settings profile version Source: https://docs.tai42.ai/api-reference/config/get-a-settings-profile-version /openapi.json get /api/config/profiles/{name}/versions/{version} One version row extended with its full ``body`` (REAL values — this door is ``secret``-fenced). The body is ``{description, env, secret_keys}``. A non-integer segment is a 400, an unknown version a 404. # List a settings profile's versions Source: https://docs.tai42.ai/api-reference/config/list-a-settings-profiles-versions /openapi.json get /api/config/profiles/{name}/versions The profile's version history as a bare array of ``{version, tags, created_at, is_current}`` rows — NO body. 404 for an absent name. # List settings groups with their resolved field values Source: https://docs.tai42.ai/api-reference/config/list-settings-groups-with-their-resolved-field-values /openapi.json get /api/config/settings-schema Return every registered settings group with per-field current values. Each group carries the settings class' field metadata plus a ``value`` per field, resolved with pydantic-settings precedence: ``os.environ`` wins (in a non-file config mode an external provider injects vars the dotenv store never sees), then the stored env override, then the shared ``TAI_DEFAULT_*`` namespace, then the field default. Nested-group reference fields (``env_var == ""``) are non-editable and report ``value: null``. Secret fields report their real value — this authed surface round-trips values through the editor; masking is display-side only, never on the wire. Each field also carries ``value_source`` naming which layer supplied its resolved ``value`` so a UI can badge provenance (notably "from default" for a ``TAI_DEFAULT_*`` fallback): ``"env"`` (process env), ``"stored"`` (the dotenv override), ``"default_namespace"`` (a ``TAI_DEFAULT_*`` fallback, from process env or the store), ``"field_default"`` (the bare field default), or ``null`` for a non-editable nested-group reference. Only IMPORTED settings classes appear: in a running skeleton the kit, skeleton, and manifest-loaded plugin modules — the intended scope. # List settings profiles Source: https://docs.tai42.ai/api-reference/config/list-settings-profiles /openapi.json get /api/config/profiles One ``{name, description}`` row per active settings profile, excluding reserved ``@``-prefixed snapshots. Reserved snapshots include e.g. ``@previous``. A store-less deploy holds no profiles, so it serves an empty list rather than opening an absent store. # Merge env overrides and reload the fleet Source: https://docs.tai42.ai/api-reference/config/merge-env-overrides-and-reload-the-fleet /openapi.json post /api/config/env Merge env overrides into the stored config and reload this worker and the fleet. A hot-class key is re-read in place. A recycle-class key rolls a worker recycle across the fleet on a supervised deployment: the response's ``recycle`` carries the roll's rows (``null`` when the change carried no recycle-class key), and when the roll converged this worker recycles itself after replying — on the HTTP edge and on every projected tool door alike (a post-reply graceful self-exit its supervisor respawns). A change the deployment cannot carry is refused with a 400 before anything is written, naming the key: a recycle-class key on a bare (unsupervised) deployment, and the bus URL or a key the deployment pins on every shape. ``secret_keys`` (when not ``None``) replaces the operator's secret marks in the same write; the marks variable itself is never accepted as an ``env`` key. Called over MCP, the reload answers on the session that called it and then retires that session with the generation it ran on: a later call on the same session fails (streamable HTTP answers it 404, and an SSE session's stream closes), so the client starts a new session to go on. # Read the active config backend mode Source: https://docs.tai42.ai/api-reference/config/read-the-active-config-backend-mode /openapi.json get /api/config/mode Return the active config backend mode. # Read the stored env config and secret-key marks Source: https://docs.tai42.ai/api-reference/config/read-the-stored-env-config-and-secret-key-marks /openapi.json get /api/config/env Return the stored env map alongside the operator's secret-key marks. ``data.env`` is the stored env key-value map (a never-written store yields an empty map). ``data.secret_keys`` is the DERIVED masked-key set (:func:`~tai42_skeleton.settings.env_secret_marks.effective_secret_keys`): the operator's stored ``TAI_ENV_SECRET_KEYS`` marks UNIONED with every live ``connectors[*].client_secret_env``, so an oauth connector's client secret is masked on display even with no operator mark. # Roll a settings profile back to a version Source: https://docs.tai42.ai/api-reference/config/roll-a-settings-profile-back-to-a-version /openapi.json post /api/config/profiles/{name}/rollback Re-point the active version to ``version`` (no data copy), making it the current body. Returns ``{ok: true, version}``. 404 for an absent name or version. A store-only re-point — the live process is realigned by a later apply, not by this door. # Soft-restart the process from its manifest Source: https://docs.tai42.ai/api-reference/config/soft-restart-the-process-from-its-manifest /openapi.json post /api/config/reload Soft-restart the process from its manifest, in-process with no pod restart. Refreshes env from the config manager, resets every settings cache, and re-initializes from the manifest. Applied on this worker (when it is a target) and broadcast to the fleet (all workers, or only ``targets``); the response embeds the per-worker fleet report. Heavy (a full re-init); meant for env/config saves, not tool edits. A convergence op: its whole purpose is aligning siblings to persisted state, so a failed local reload still broadcasts and then re-raises with the fleet report attached. Called over MCP on a worker the reload applies to (no ``targets``, or ``targets`` naming that worker), the reload answers on the session that called it and then retires that session with the generation it ran on: a later call on the same session fails (streamable HTTP answers it 404, and an SSE session's stream closes), so the client starts a new session to go on. When ``targets`` leave that worker out, nothing reloads there and the session keeps working. Each worker's payload is ``{status, env_keys, awaiting_respawn}``: ``awaiting_respawn`` names the recycle-class keys whose value now differs from the one that worker bound (at boot, or when the plugin that declares them was loaded) — the soft restart carries every hot key in place, and those keys take effect only when the worker is respawned through the deployment's supervisor (on a bare deployment, a server restart). This operation ERROR-logs one line naming every such worker and its keys. # Add an MCP server as a connector provider Source: https://docs.tai42.ai/api-reference/connectors/add-an-mcp-server-as-a-connector-provider /openapi.json post /api/connectors/providers Add the MCP server at ``url`` as a discovered provider, written to the manifest and broadcast to the fleet. Its id is ``provider_id`` or a suggestion from the host. An id that is not free — a provider in the manifest, a stored connection record (live or lapsed), or a stored client row under it — is a 409 naming which; nothing is deleted. # Complete an OAuth connection flow Source: https://docs.tai42.ai/api-reference/connectors/complete-an-oauth-connection-flow /openapi.json post /api/connectors/oauth/complete Complete an OAuth connection flow, serving the discriminated ``/oauth/complete`` body. # Disconnect a connection Source: https://docs.tai42.ai/api-reference/connectors/disconnect-a-connection /openapi.json delete /api/connectors/connections/{connection_id} Disconnect (purge) a connection; a genuinely-absent connection is a 404. # Get a connection Source: https://docs.tai42.ai/api-reference/connectors/get-a-connection /openapi.json get /api/connectors/connections/{connection_id} One connection's secret-free view, with live sub-service reachability. An unknown id is a loud 404. # List connections Source: https://docs.tai42.ai/api-reference/connectors/list-connections /openapi.json get /api/connectors/connections The installed connections as secret-free views. ``health`` filters the returned items to a single ``auth_health_state`` (``healthy`` / ``reconnect_required`` / ``refresh_failing``); an unrecognised value is a loud 400. ``limit`` caps the number of returned items (a positive integer no larger than the hard guard); ``total`` still reports the full match count so a caller sees there is more beyond the page. ``unhealthy`` counts every not-healthy connection across the whole set, independent of the filter and limit. # List connector providers Source: https://docs.tai42.ai/api-reference/connectors/list-connector-providers /openapi.json get /api/connectors/providers The provider catalog — one entry per registered connector provider, plus category groupings. The category groupings are those the UI arranges the providers under. Providers come from the in-memory registry (populated from the manifest's ``connectors`` list at boot/reload), so they list regardless of store configuration; each says whether the provider door may remove it and where its sign-in client comes from (``unset`` for a stored-client provider and never removable while the store is not configured). The category groupings live in the connector store's Postgres, so they are served only when that store is configured (otherwise an empty grouping list, mirroring the OFF-state connections read). # Look up an MCP server's sign-in Source: https://docs.tai42.ai/api-reference/connectors/look-up-an-mcp-servers-sign-in /openapi.json post /api/connectors/providers/discover Look up the sign-in of the MCP server at ``url``, or of the server of the discovered provider ``provider_id``. Persists nothing. ``origin`` is the deployment origin a sign-in would start from: it decides the callback address an app registered by hand must allow and whether this deployment's client-identity document can serve the login. An unknown provider is a 404; a declared one a 400. # Patch a connection's enabled sub-services Source: https://docs.tai42.ai/api-reference/connectors/patch-a-connections-enabled-sub-services /openapi.json patch /api/connectors/connections/{connection_id}/sub-services Toggle a connection's enabled sub-services; an unknown id is a 404. # Re-encrypt connector tokens and client credentials under the current KEK Source: https://docs.tai42.ai/api-reference/connectors/re-encrypt-connector-tokens-and-client-credentials-under-the-current-kek /openapi.json post /api/connectors/tokens/reencrypt Re-encrypt every stored connector token and client credential under the current ``CONNECTORS_KEK``. The KEK-rotation convergence sweep: after ``CONNECTORS_KEK`` is rotated (new current key, old key moved to ``CONNECTORS_KEK_PREVIOUS``), this rewrites every connection blob and every stored OAuth client blob still under a previous key so the previous key can be retired. Idempotent — a blob already under the current key is skipped. A blob no configured key can open is counted and named in ``failed_connection_ids`` / ``failed_client_row_ids``, never silently dropped. A missing/malformed current ``CONNECTORS_KEK`` is a deployment-wide config fault and surfaces as a named ``NotSupportedError`` (501), never an unnamed 500. # Remove a connector provider added by its address Source: https://docs.tai42.ai/api-reference/connectors/remove-a-connector-provider-added-by-its-address /openapi.json delete /api/connectors/providers/{provider_id} Remove a removable provider and its stored clients. A provider that is not removable here is a 409 naming where it is managed; a provider with a LIVE connection is a 409 naming the count. A connection whose session has lapsed is purged first, through the disconnect routine. The manifest change comes before the client rows are deleted, so a failed change leaves them; a failed deletion after it is raised, and running the remove again deletes the rows left under the id (nothing else holds it any more; no manifest change, so no fan-out). # Set the client credentials of a connector provider Source: https://docs.tai42.ai/api-reference/connectors/set-the-client-credentials-of-a-connector-provider /openapi.json put /api/connectors/providers/{provider_id}/client Store the client credentials of an app registered by hand for the discovered provider ``provider_id``. The credentials are stored for one sign-in service the provider's server lists: ``issuer`` when given (400 when the server does not list it), else the one the provider's manual client is already stored for while the server still lists it; with neither, a 400 ``connector-client-issuer-required`` naming the listed ones — the secret is never sent to a sign-in service nobody chose. Only a discovered provider that names no env client takes stored credentials. # Start a connection flow Source: https://docs.tai42.ai/api-reference/connectors/start-a-connection-flow /openapi.json post /api/connectors/connections/start Begin a Connect (OAuth authorize URL, or an immediate no-auth connection). # Start a reconnect flow Source: https://docs.tai42.ai/api-reference/connectors/start-a-reconnect-flow /openapi.json post /api/connectors/connections/{connection_id}/reconnect Re-run the OAuth flow for an existing connection; an unknown id is a 404. # Create or replace a conversation route Source: https://docs.tai42.ai/api-reference/conversations/create-or-replace-a-conversation-route /openapi.json post /api/conversations/{route_name} Create a conversation route from its flat parameters — an UPSERT (create AND edit path). ``initial_mode`` is the route's default control mode (``agent`` runs the turn, ``manual`` suppresses it for an operator to answer) when a thread carries no per-thread override. ``target_name`` must merely EXIST — the agent (``target_kind=agent``) or tool (``target_kind=tool``) — the key's live grants bound the turn at fire. ``execution_key`` is the api-key identity the turn runs AS; the caller must be allowed to delegate it and it must be usable by a tokenless fire. Both are decided at this door BEFORE the route write service runs the route's own rules, so a refusal leaves any existing row untouched. The write service runs the target's bind check (a refusal is a 422 carrying its lines), then renders and compiles the door jqs (``start_expr``/``cancel_expr``/``resume_expr``/``extras_expr``) and ``reply_expr``, so an invalid one — or a by-id text whose stored resource cannot be fetched — is refused at create, not at first message. A ``channel`` row's ``our_identity`` is stored canonicalized and must not already be routed on that channel. An edit that would change the ``door`` of a route already HOLDING threads is refused: the two doors key their threads differently, so the held threads cannot be re-keyed under the new door. An ``api`` row that declares a ``callback_url`` has its ``callback_secret`` minted by the write and returned ONCE; a poll-only api row (no callback) mints none and reads its answers back from the poll door. A positive ``turns_per_hour_override`` runs this route's per-address buckets at that rate instead of the global ``per_address_turns_per_hour`` cap; ``None`` runs them at the global rate. A ``reply_texts`` catalogue carries the route's own words for the platform's fixed participant replies (a failed turn, the slow-down line, the pairing replies, the inbound refusal notices), in the line's language; ``None`` or an unset member sends the built-in English reply. A ``locale`` is the operator-declared default language templated reply parts render in when a turn supplies none; it is the last fallback under a per-turn or stored-contact locale and is stored canonicalized. ``None`` declares no route default (bare/English). An ``overlap`` policy governs how a running turn treats the newer participant messages that overlap it (learn/cancel/carry); ``None`` stores the default policy (continue/one/no window), which runs one turn per message and leaves the payload unchanged. Returns ``{"created", "route_name", "route", "callback_secret"}``. # Create or replace a conversation target config Source: https://docs.tai42.ai/api-reference/conversations/create-or-replace-a-conversation-target-config /openapi.json put /api/conversation-configs/{target_kind}/{target_name} Create or replace the per-target config for ``(target_kind, target_name)`` — an UPSERT. This is the create path AND the edit path for a config of that key. The target must merely EXIST — the agent (``target_kind=agent``) or tool (``target_kind=tool``) — exactly as the route create checks it. ``greeting_template`` may reference at most the ``{pairing_code}`` placeholder and a blank template is refused (``null`` = no greeting), both enforced by the model. No key is bound and no authority delegated: this is inert operator config. Returns ``{"created", "target_kind", "target_name", "config"}``. # Delete a conversation person Source: https://docs.tai42.ai/api-reference/conversations/delete-a-conversation-person /openapi.json delete /api/conversations/persons/{person_id} Erase a LINKED person and every person it absorbed by a link from the bridge's stores. - the person's aggregated ``bridge:@person:{person_id}`` thread — its agent checkpoint, and across EVERY route the person wrote under (its addresses' routes and the routes its thread's writes recorded) its answer records, per-thread transcript indexes, route-thread index memberships and per-thread mode override (the thread-delete machinery, reused); - for every person it absorbed by a ``/link`` (a chain flattened into it): that person's aggregated thread the same way; - every route-keyed thread, on any route, that only the person and the persons it absorbed wrote in — its checkpoint, records, parks and ``thread``-subject state, exactly as a thread delete forgets it. The thread's records decide, read under its hold: a thread a record of another writer still stands in (another person, or an address nobody held when it wrote) is left to them, unless that person's own erase is running too, and nothing is written to a thread left; - the declared state of the person and of every person it absorbed (``person``-subject and their aggregated threads' ``thread``-subject memory); - the person row ``conversations:person:{person_id}``, its absorbed SET, the absorbed persons' pointers, their lists of the threads they wrote in, and every ``person_index`` ``door_address_key → person_id`` mapping of its addresses. The bridge's stores only: the interaction store's answered state hashes (which keep a request's ``recipient`` and a non-sensitive answer until their idle TTL) and the hook-runs store (whose failed run can carry a participant's address in its event payload until an operator retries or discards it) are not erased by it. Caller authority is the door's grantable ``write`` action — the same grant that forgets a thread. IDEMPOTENT and retryable: the erase first sets the person's erase mark under the per-thread FIFO, and from then on every person write refuses the person, its addresses are attributed as unlinked and its thread takes no new record; the person row is the durable marker of an owed erase and is deleted LAST (atomically with its mark, its index mappings, its absorbed SET and the lists of the threads they wrote in), so an interruption after the teardown began leaves the row and the mark and a retry FINISHES. A refusal before the teardown (a 409 or a 503) removes the mark first. A write by the person or a person it absorbed that lands in a thread the erase had not reached, in its last moment, is a 409 after the teardown: the mark stands, and a retry finishes the erase and clears that thread too. A retry, or an id that was never a person, is not a 404 — its aggregated checkpoint is forgotten regardless (it defaults to keep-forever) and the call answers ``erased=false``. An id a ``/link`` absorbed into another person is a 409 naming that person (``absorbed_into_person_id``), whose erase clears it. A turn IN FLIGHT on a thread the erase clears is a 409 (retry once it drains), re-checked under the thread's FIFO before its teardown so an admitted turn cannot re-create memory behind the erase; a full queue is the retriable 503. As a live-caller sync door every acquisition is bounded by ``sync_door_wait_seconds``: a wait past it — behind a turn possibly HITL-paused on another worker, behind a turn on an absorbed person's thread that holds its lock while it waits for a gate slot, or behind a ``delete-person`` call of an absorbed id — is the loud, retriable 503 ``ThreadBusyError`` rather than a block past the proxy timeout. A blank ``person_id`` is a 400; no backend is a loud 501. Returns ``{"person_id", "removed", "erased", "states_erased", "absorbed_person_ids"}``, where ``removed`` counts the answer records this call deleted across the person's routes, in every absorbed person's thread and in every route thread it cleared, ``erased`` says whether THIS call removed the person row, ``states_erased`` counts the declared-state records forgotten for the person and every absorbed person, the cleared route threads' ``thread`` subjects included, and ``absorbed_person_ids`` lists, sorted, the absorbed ids this call cleared. A send still going out to one of the person's addresses keeps that address on its ``sending`` row until its completion, end or settle (at most one settle deadline), and ``removed`` does not count it. A completion re-attributed once the erase has begun is placed without the person link, in the route-keyed thread an inbound from that address would now start. # Delete a conversation route Source: https://docs.tai42.ai/api-reference/conversations/delete-a-conversation-route /openapi.json delete /api/conversations/{route_name} Delete a conversation route by name, along with the thread indexes it owned. Those indexes carry no TTL and the prune pass only walks LIVE routes, so a delete that left them behind would strand them unreachable forever; the answer records they name keep their own retention TTL and are not touched. The reclamation is RETRYABLE, because it can be interrupted (a socket timeout, a SIGTERM, a Redis blip) after the routing row is already gone: a name whose route index survives is one whose reclamation is still owed, so this door re-runs it and answers ``removed=false`` rather than the 404 that would leave those keys unnameable forever. Only a name that neither routes nor owes reclamation is the loud 404. A name that is not a valid slug is a 400. Returns ``{"removed", "route_name"}``, where ``removed`` says whether THIS call removed the routing row. # Delete a conversation target config Source: https://docs.tai42.ai/api-reference/conversations/delete-a-conversation-target-config /openapi.json delete /api/conversation-configs/{target_kind}/{target_name} Delete the per-target config for ``(target_kind, target_name)``. An unknown key is a loud 404; a malformed key is a 400. Returns ``{"removed", "target_kind", "target_name"}``. # Delete a conversation thread Source: https://docs.tai42.ai/api-reference/conversations/delete-a-conversation-thread /openapi.json delete /api/conversations/{route_name}/thread Forget ONE conversation thread: its agent checkpoint, its answer records and its thread indexes. A later message on the same address starts a memory the deleted turns never touched. A LINKED person's aggregated ``bridge:@person:{id}`` thread is forgotten across every route index it spans. Forgetting is ABSOLUTE: a valid id on its own route always succeeds, even when nothing is stored. An aged-out thread whose answer records already expired under the retention TTL, or one never seen, answers ``removed=0`` and never a 404 — and its checkpoint is deleted regardless, because that memory defaults to keep-forever and is otherwise left behind once the records lapse. A route-keyed id MUST carry the route's ``bridge:{route_name}:`` prefix; an id belonging to another route is a loud 400, the sole guard stopping a delete on one route from wiping another route's memory. A person ``bridge:@person:{id}`` thread whose person is unknown, or whose ``route_name`` is not one of the person's routes, is a loud 404. A turn IN FLIGHT on the thread (an ``accepted`` intake still holding a live lease) is a 409: its completion re-writes checkpoint state and re-stamps the indexes behind the delete, half-forgetting the memory — retry once it drains. The guard is re-checked under the FIFO, so a turn admitted while the lock is being taken is refused before any teardown, never left to re-create memory behind the delete. A ``route_name`` that is not a valid slug, or a blank ``thread_id``, is a 400. Caller authority is the door's grantable write action — the SAME write grant that creates a route deletes routes and forgets threads — never a per-thread owner check. The teardown runs in an order an interruption cannot strand: the checkpoint FIRST — its memory is reachable only through the indexes once this call returns, so it must go while the indexes still name the thread to bring a re-run back — then the records and the indexes together, the index being the durable marker of an owed reclamation and torn down LAST. Idempotent under re-run, so a partial completion is FINISHED by a retry. The whole teardown runs under the thread's per-thread FIFO (the lock an operator send and an in-flight turn take), so an operator send already in flight on the thread within this worker drains BEFORE the delete rather than half-behind it; a full queue is the loud, retriable 503 that FIFO raises before anything is torn down. As a live-caller sync door the acquisition is bounded by ``sync_door_wait_seconds``: a wait past it — behind a turn possibly HITL-paused on another worker — is the loud, retriable 503 ``ThreadBusyError`` rather than a block past the proxy timeout. Returns ``{"removed", "route_name", "thread_id", "states_erased"}``, where ``removed`` counts the answer records this call deleted (0 when a prior run already cleared them, when their rows had expired under the retention TTL, or when the id was never stored) and ``states_erased`` counts the thread-subject declared-state records forgotten across the routes the thread spans (0 when the states feature is off or the thread held no declared state). A send still going out on the thread when this call runs is forgotten when it ends, not by this call: for at most one settle deadline its ``sending`` row, in no thread index, still holds the participant's address, and ``removed`` does not count it. # Deliver a structured event to a conversation thread as a turn Source: https://docs.tai42.ai/api-reference/conversations/deliver-a-structured-event-to-a-conversation-thread-as-a-turn /openapi.json post /api/conversations/{route_name}/events Run a structured event as a turn on an EXISTING thread of ``route_name``. The thread must already exist: an event enters a conversation as a turn and never mints a thread. Address it by the ``address`` its listing shows or by its ``thread_id`` (exactly one). The turn runs the route's TOOL target AS the route's execution key — an agent-target route is refused (``409``) since an event carries no rendered text. The event is IDEMPOTENT on ``event_id``: a redelivery returns the original turn's ``message_id`` (``202``) and runs no second turn. The answer is delivered by the TARGET route's door — a channel route texts the thread's address, an api route POSTs the route's signed callback (a ``wait_seconds`` body field, clamped to ``sync_wait_max_seconds``, returns a finished turn's answer inline in the ``200`` and suppresses the callback, but only while the client is still connected — a hung-up caller's answer falls to the callback or its terminal poll record). Default is ``202 {message_id, thread_id}``. This is a TRUSTED-integration door: an authorized writer may address ANY existing thread of the route by ``thread_id`` (a channel participant's included), so a deployment grants its write action to service principals, not to low-trust API keys. # Get a conversation route Source: https://docs.tai42.ai/api-reference/conversations/get-a-conversation-route /openapi.json get /api/conversations/{route_name} One conversation route by name, with its ``callback_secret`` withheld. An unknown name is a loud 404; a name that is not a valid slug is a 400. # Get a conversation target config Source: https://docs.tai42.ai/api-reference/conversations/get-a-conversation-target-config /openapi.json get /api/conversation-configs/{target_kind}/{target_name} One per-target config by ``(target_kind, target_name)``. An unknown key is a loud 404; a key whose ``target_kind`` is not a known kind, or whose ``target_name`` is blank, is a 400. # List a conversation route's threads Source: https://docs.tai42.ai/api-reference/conversations/list-a-conversation-routes-threads /openapi.json get /api/conversations/{route_name}/threads The threads of ``route_name``, newest activity first, one page at a time. A thread listing spans every caller and address on the route, so it is admin-only. Each item carries ``thread_id``, ``client_address``, ``message_count`` and ``last_delivery_status`` summarized from the thread's newest readable record, plus ``last_activity_at`` — the route index's own score, which is what this listing SORTS by, so the moment shown and the position it is shown in always agree. That score is stamped when a record is created and again when its turn completes; a later delivery transition on the same record moves the record's ``updated_at`` but not the thread's activity. ``status`` (one of the delivery-status vocabulary) keeps only threads whose summary status matches; ``address`` keeps only threads whose id client-address suffix contains that substring. Neither has a direct index — the per-status indexes are GLOBAL over ``message_id``s, not per-route thread ids — so a filter is a BOUNDED app-side post-scan and a page that spent its scan budget answers ``truncated: true`` LOUDLY, never a silent cut. An unknown ``status`` is a loud 400. Authorization is decided BEFORE the route is looked up, so a non-admin is refused the same way whether the name routes or not — a 404-here/403-there pair would answer which route names exist to a caller with no business knowing. An unknown route is a loud 404 to an admin; a ``page`` or ``page_size`` below 1, or a ``page`` above the served maximum, is a 400. Returns ``{"items", "total", "page", "page_size", "next_page", "truncated", "unreadable"}``, where ``total`` counts the route's indexed threads for an unfiltered listing, or the matches the bounded scan found for a filtered one, and ``unreadable`` counts the threads with no readable record. # List conversation routes Source: https://docs.tai42.ai/api-reference/conversations/list-conversation-routes /openapi.json get /api/conversations Every stored conversation route, each with its ``callback_secret`` withheld. Returns ``{"items", "total", "unreadable"}``, where ``unreadable`` counts the indexed routes whose row was gone or unparseable. # List conversation target configs Source: https://docs.tai42.ai/api-reference/conversations/list-conversation-target-configs /openapi.json get /api/conversation-configs Every stored per-target conversation config. Returns ``{"items", "total", "unreadable"}``, where ``unreadable`` counts the indexed configs whose row was gone or unparseable. # List failed conversation deliveries Source: https://docs.tai42.ai/api-reference/conversations/list-failed-conversation-deliveries /openapi.json get /api/conversations/messages/failed List every answer record whose delivery ended ``failed``. The listing spans every route and caller, so it is admin-only. Returns ``{"items", "total", "unreadable"}``, where ``unreadable`` counts the indexed failed members whose row was gone or unparseable. # Read a conversation person Source: https://docs.tai42.ai/api-reference/conversations/read-a-conversation-person /openapi.json get /api/conversations/persons/{person_id} The person row named by ``person_id`` — its identity, folded addresses and stored ``locale``. ``locale`` is the BCP 47 tag the rendering layer resolves text against, or ``null`` when none is known. The subject read that serves a person's locale. A blank ``person_id`` is a 400; an unknown one a 404; no backend a loud 501. # Read a conversation thread's control mode Source: https://docs.tai42.ai/api-reference/conversations/read-a-conversation-threads-control-mode /openapi.json get /api/conversations/{route_name}/thread/mode The mode in force for ``thread_id`` on ``route_name`` and where it comes from. Returns ``{"mode", "source"}``, ``source`` being ``thread`` for a per-thread override and ``route`` for the no-override default. A route-keyed thread's default is the route's ``initial_mode``; a LINKED person's aggregated thread defaults to ``manual`` when ANY route the person spans defaults to ``manual``, else ``agent``. The thread-belongs-to-route guard is the thread delete's: a route-keyed id off the route is a 400, a person thread off the named route a 404. An unknown route is a loud 404; a blank ``thread_id`` a 400. # Read a conversation thread's transcript Source: https://docs.tai42.ai/api-reference/conversations/read-a-conversation-threads-transcript /openapi.json get /api/conversations/{route_name}/transcript One thread's records under ``route_name``, one page at a time. ``order`` picks the direction: ``asc`` (the default) reads the transcript oldest first, ``desc`` reads it newest first, which is the order a live tail wants because page 1 then always holds the latest messages. ``page``/``page_size`` window that order from its own end, so page 1 of ``desc`` is the newest page and never the oldest. ``q`` filters to records whose inbound text or answer contains that substring — a BOUNDED scan (the searched text lives inside the record content blob), so a page that spent its scan budget answers ``truncated: true`` LOUDLY. A ``q`` read never 404s: the unknown-thread 404 below is gated on an UNFILTERED read, so under ``q`` an unknown thread and one that matched nothing alike read as an EMPTY page. An admin reads whole records; a non-admin reads the caller-safe projection, which withholds the internal detail of the route key's run. An unknown ``route_name`` is a loud 404. A thread that is absent or keyed to another route answers the uniform thread not-found. A ``page`` or ``page_size`` below 1, a ``page`` above the served maximum, a blank ``thread_id`` or an unknown ``order`` is a 400. A thread the index still holds but whose rows have expired under the retention TTL is NOT that 404: it reads as an empty page carrying the indexed ``total``, until the prune pass reclaims the members and the thread becomes unknown. Returns ``{"items", "total", "page", "page_size", "next_page", "order", "truncated", "unreadable", "person_id"}``, where ``total`` counts the thread's indexed records for an unfiltered read, or the matches a bounded ``q`` scan found, ``unreadable`` counts the members whose row was gone or unparseable, and ``person_id`` names the linked person of a person thread (``None`` for a route-keyed thread). # Read one conversation answer record Source: https://docs.tai42.ai/api-reference/conversations/read-one-conversation-answer-record /openapi.json get /api/conversations/{route_name}/messages/{message_id} One conversation answer record by ``message_id`` under ``route_name``. A missing record or one on another route is a 404. An admin reads the whole record; a non-admin reads the caller-safe projection, which withholds the internal detail of the route key's run. # Search a conversation route's messages Source: https://docs.tai42.ai/api-reference/conversations/search-a-conversation-routes-messages /openapi.json get /api/conversations/{route_name}/messages/search Return every record on ``route_name`` whose inbound text or answer contains ``q``, one page at a time. Across ALL the route's threads. The search spans every caller and address on the route, so it is admin-only, and each item is the WHOLE record (the same shape the transcript serves an admin). ``q`` is REQUIRED and non-blank. There is no per-route record index, so the search is a BOUNDED nested scan of the route's threads and their records; a page that spent its scan budget answers ``truncated: true`` LOUDLY, never a silent cut. Authorization is decided BEFORE the route is looked up, so a non-admin is refused the same way whether the name routes or not. An unknown route is a loud 404 to an admin; a blank ``q``, a ``page``/``page_size`` below 1, or a ``page`` above the served maximum, is a 400. Returns ``{"items", "total", "page", "page_size", "next_page", "truncated", "unreadable"}``, where ``total`` is the matches the bounded scan found and ``unreadable`` counts the scanned members whose row was gone or unparseable. # Send a message to a conversation route Source: https://docs.tai42.ai/api-reference/conversations/send-a-message-to-a-conversation-route /openapi.json post /api/conversations/{route_name}/messages Accept one authed message for ``route_name`` and run its turn AS the route's execution key. The auth gate authorizes who may SEND; the turn's own authority is the route's execution key, not the caller. Default answer is ``202 {message_id, thread_id}``; the answer is then delivered later by signed callback when the route declares a ``callback_url``, and otherwise read back from the poll door (``GET .../messages/{message_id}``). With a ``wait_seconds`` body field (clamped to ``sync_wait_max_seconds``) a turn finishing in time answers ``200`` inline — an answered/error turn with the answer, a silent turn with the silent marker (status ``silent``, no answer text) — and any callback (which otherwise carries answered/error/silent) is suppressed the same way, so it never double-fires; a turn still running when the wait elapses falls back to ``202``. The inline ``200`` is taken only while the client is still connected — a caller that hung up during the wait gets its answer by the route's signed callback (or its terminal poll record), never a write to a closed socket. # Send an operator message into a conversation thread Source: https://docs.tai42.ai/api-reference/conversations/send-an-operator-message-into-a-conversation-thread /openapi.json post /api/conversations/{route_name}/thread/messages Send a message BY HAND into ``thread_id`` on ``route_name`` as the route identity. Returns ``{"message_id", "thread_id"}``. No turn runs: the message is stored already ``answered`` and delivered through the same machine a produced answer takes. ``media`` (a list of ``{"kind", "url", "caption"?, "filename"?}`` display items), ``location`` (a shared map pin ``{"latitude", "longitude", "name"?, "address"?}``), ``template`` (a pre-approved out-of-window template ``{"name", "language", "parameters"?}`` — ``parameters`` is the channel-owned opaque runtime-argument object), ``options`` (a list of FLAT tappable option objects — each a ``{"kind": "reply", "text"}`` reply or a ``{"kind": "link", "label", "url"}`` link action), ``sections`` (the SECTIONED tappable-options alternative — titled groups of reply rows), ``header``/``footer`` (a media header / trailing line composing an interactive message) and ``schema`` (an ask-less form's answer schema — the channel renders ``text`` as the form's prompt, and the participant's submission enters the conversation as an ordinary inbound message) are OPTIONAL richer-send forms — FULL parity with the flow answer path's ``AnswerPart`` vocabulary — delivered ALONGSIDE ``text`` — the message is then stored and delivered as one rich part, exactly as a produced rich answer is, including the delivery machine's capability gate (a channel that does not advertise the matching ``supports_*_notifications`` flag never receives the part; the record fails loudly instead of the field dropping). A contract-invalid value (an empty list/dict, an over-cap value, or a combination the shared composition matrix refuses — ``options`` XOR ``sections``, ``schema`` excludes both, ``header``/``footer`` require a choice surface, ``template`` standalone) is a loud 400; omit them all for a plain text send. Allowed in either mode and it never flips the mode. Blank ``text`` is a loud 400, and a present-but-blank ``address`` is a 400. The thread-belongs-to-route guard is the thread delete's: a route-keyed id must carry the route's ``bridge:{route_name}:`` prefix (400 otherwise), a person thread must be on the named route (404 otherwise). ``address`` picks the send target on a LINKED person's aggregated thread — it must be one of the person's addresses (400 otherwise); with no ``address`` the target is the thread's newest record that is not a send record, and a person thread with no such record and no ``address`` is a 400. For an agent target that holds thread memory the message is appended to the thread's checkpoint as an ``assistant`` reply BEFORE the record is created; an append that fails is a loud 500 and no record is created. The send takes the thread's per-thread FIFO, so it waits behind an in-flight turn and never interleaves it; a full queue is a loud, retriable 503. As a live-caller sync door the wait to acquire the slot is bounded by ``sync_door_wait_seconds``: a wait past it — behind a turn possibly HITL-paused on another worker — is the loud, retriable 503 ``ThreadBusyError`` rather than a block past the proxy timeout. Caller authority is the door's grantable ``write`` action — the same write grant that forgets threads — and the record names the calling operator. An unauthenticated caller (access control disabled or unbound) is a loud 501, since an operator action must be attributable. # Set a conversation person's locale Source: https://docs.tai42.ai/api-reference/conversations/set-a-conversation-persons-locale /openapi.json put /api/conversations/persons/{person_id}/locale Set (or clear) a person's stored ``locale``. The operator override the rendering layer resolves text against, winning over the channel-seeded value on every later turn. ``locale`` is a BCP 47 tag (canonicalized here — ``he-il`` stores as ``he-IL``); ``null`` clears it back to no-locale-known. A blank ``person_id`` or a malformed ``locale`` is a 400; an unknown person a 404; a person being erased a 503 with nothing changed; no backend a loud 501. Returns the updated person. # Set a conversation thread's control mode Source: https://docs.tai42.ai/api-reference/conversations/set-a-conversation-threads-control-mode /openapi.json put /api/conversations/{route_name}/thread/mode Set the per-thread mode override for ``thread_id`` on ``route_name`` to ``mode``. ``mode`` is one of ``agent``/``manual``. Returns ``{"route_name", "thread_id", "mode", "source"}`` with ``source`` always ``thread`` — a set writes an override. This is the door an EXTERNAL/programmatic caller names a thread through; an agent flipping its OWN live conversation uses the ``set_conversation_mode`` builtin instead, which reads the current thread from the turn context rather than naming it. Caller authority is the door's grantable ``write`` action. The thread-belongs-to-route guard is the thread delete's: a route-keyed id off the route is a 400, a person thread off the named route a 404. An unknown route is a loud 404; a blank ``thread_id`` or a ``mode`` outside the vocabulary is a 400. # Add or remove single extension combos on a tool Source: https://docs.tai42.ai/api-reference/extensions/add-or-remove-single-extension-combos-on-a-tool /openapi.json post /api/tools/{name}/extensions/combos Granularly edit a tool's extension combos: append ``add`` and drop ``remove``. Applied over the merge of its current combos, then persisted through the same pipeline ``set_tool_extensions`` rides. Pinned order (each check loud, nothing persisted on a reject): the tool must be REGISTERED (404, as ``get_tool_extensions``); its owning config resolves (400/400/409, as ``set_tool_extensions``); membership over the OWNING config's combos in their stored form — adding a present combo → 400, removing an absent one → 404, each naming the combo; the MERGED list validates against the live registry (400) before any write. Returns the tool's full new combos in the GET door's shape. # Get a tool's applied extension combos Source: https://docs.tai42.ai/api-reference/extensions/get-a-tools-applied-extension-combos /openapi.json get /api/tools/{name}/extensions Return the tool's applied extension combos and the available extensions; 404 for an unknown tool. # List every registered extension Source: https://docs.tai42.ai/api-reference/extensions/list-every-registered-extension /openapi.json get /api/extensions Return the flat ``{"name", "kind", "multiple"}`` list of every registered extension for the picker. # Set a tool's applied extension combos Source: https://docs.tai42.ai/api-reference/extensions/set-a-tools-applied-extension-combos /openapi.json post /api/tools/{name}/extensions Author all of a tool's extension combos through the pipeline (persist, reload, broadcast). Owner resolution, the consolidation-conflict check, and the registry combo validation all run BEFORE the pipeline (nothing persisted on a reject). The mutator then upserts the providing config's ``extensions`` entry, and ConfigService validates the whole document, persists, reloads locally, and broadcasts the reload so every worker rebinds; the per-worker fleet report rides the response as its ``fanout`` summary. The persist + local reload have already landed, so re-running the apply (or a ``reload_config``) is the recovery for a sibling that did not converge. # Bind a webhook verifier to a topic Source: https://docs.tai42.ai/api-reference/hooks/bind-a-webhook-verifier-to-a-topic /openapi.json put /api/hooks/topics/{topic}/verifier Bind (or replace) a topic's webhook verifier. The verifier name is resolved against the registry here — an unknown name is rejected at BIND time (400), never left to fail at delivery. # Create a trigger link Source: https://docs.tai42.ai/api-reference/hooks/create-a-trigger-link /openapi.json post /api/hooks/trigger-links Mint a trigger link for ``topic``. ``ttl_seconds`` is the creator's explicit choice (``null`` permanent, positive timed; ``0``/negative → 400); a unique name is generated when omitted; a verifier-bound topic is refused (400); ``tool_kwargs`` rides every fire, filling only the arguments each fired hook's author left unpinned. ``execution_key`` is the api-key identity the link's dispatch is gated on, decided BEFORE the mint so a refusal leaves no live URL. ``require_api_key`` makes the link's door demand an authenticated caller beside the token. Returns ``{"name", "trigger_path", "token", "topic", "expires_at"}`` — the token appears ONLY here (nothing else stores or lists it). ``created_by`` is stamped from the AMBIENT caller identity, never a request field (which would be caller-spoofable); with the gate off there is no principal and it is stored ``null``. # Delete a trigger link Source: https://docs.tai42.ai/api-reference/hooks/delete-a-trigger-link /openapi.json delete /api/hooks/trigger-links/{name} Revoke a trigger link by name — immediate and DURABLE. A permanent tombstone keeps a restored backup from re-arming it. An unknown name is a loud 404. Returns ``{"removed", "name"}``. # Discard a failed hook run Source: https://docs.tai42.ai/api-reference/hooks/discard-a-failed-hook-run /openapi.json delete /api/hook-runs/{id} Delete a failed hook run for good: its hook never runs again for that event. # List hook runs Source: https://docs.tai42.ai/api-reference/hooks/list-hook-runs /openapi.json get /api/hook-runs Kept hook runs, with the outstanding and failed totals; never their payloads. ``status`` ``outstanding`` lists the pending and running runs, the soonest due first; ``failed`` the failed runs, the most recently failed first; none lists both, outstanding first. A page holds at most ``limit`` runs; ``cursor`` continues from a page's ``next_cursor``. # List registered hooks Source: https://docs.tai42.ai/api-reference/hooks/list-registered-hooks /openapi.json get /api/hooks List registered hooks plus the per-topic verifier bindings. With a topic, lists only the hooks registered for that topic; without one, lists every registered hook. The response carries the hooks under ``items`` (with ``total``), the per-topic verifier bindings under ``topic_verifiers``, and under ``trigger_auth`` how a topic's webhook ingress door authenticates its caller — DERIVED from the live verifier bindings just read, never stored. ``trigger_auth`` is keyed by every topic among the LISTED hooks plus every topic currently carrying a verifier binding (one may be bound before any hook exists on it), so under a ``topic`` filter it is neither a subset nor a superset of the listed topics. # List registered webhook verifiers Source: https://docs.tai42.ai/api-reference/hooks/list-registered-webhook-verifiers /openapi.json get /api/hooks/verifiers The sorted names of every registered webhook verifier — the catalog the Studio bind form offers. Offered instead of free text. Names ONLY: a verifier object and a bound config are secret-adjacent and never leave this door. An empty registry (no verifier lifecycle module loaded) is a valid state, so it returns ``[]``. # List trigger links Source: https://docs.tai42.ai/api-reference/hooks/list-trigger-links /openapi.json get /api/hooks/trigger-links Every live trigger link's record plus its hash PREFIX and its derived ``trigger_auth``. Never a raw token (none is stored; a listed link's QR is unrecoverable by design). Returns ``{"items", "total"}``. # Register a hook Source: https://docs.tai42.ai/api-reference/hooks/register-a-hook /openapi.json post /api/hooks Register a hook from its flat parameters — an UPSERT (the create path and the edit path). Supports conditional execution (``condition``) and the four door-contract expressions (``start_expr`` builds the fired tool's kwargs; ``cancel_expr`` / ``resume_expr`` act on the run's parked interactions; ``extras_expr`` builds the started run's extras) — each a templated text carrying its jq inline or by stored id — plus dynamic tool arguments. ``execution_key`` is the api-key identity every fire runs as; the caller's authority to delegate it and its usability by a tokenless fire are decided BEFORE the upsert, so a refusal stores nothing and leaves any existing hook of that name untouched. Returns ``{"registered", "name"}``; ``registered`` is ``True`` for a create and a replace alike. # Retry a failed hook run Source: https://docs.tai42.ai/api-reference/hooks/retry-a-failed-hook-run /openapi.json post /api/hook-runs/{id}/retry Re-queue a failed hook run as a fresh run with the same id, and so the same idempotency key. A run whose hook started its tool replays that start alone; any other runs its hook again on the current parked entries. A run whose hook committed a resume or a take is refused: retrying it would act twice, so it can only be discarded. Answers the re-queued run. # Unbind a topic's webhook verifier Source: https://docs.tai42.ai/api-reference/hooks/unbind-a-topics-webhook-verifier /openapi.json delete /api/hooks/topics/{topic}/verifier Remove a topic's verifier binding, reopening its public ingress door. A missing binding is a loud 404. # Unregister a hook Source: https://docs.tai42.ai/api-reference/hooks/unregister-a-hook /openapi.json delete /api/hooks/{name} Unregister a hook by name. An unknown hook name is a loud 404. Returns ``{"removed", "name"}``. # Answer a pending interaction Source: https://docs.tai42.ai/api-reference/interactions/answer-a-pending-interaction /openapi.json post /api/interactions/{interaction_id}/answer Answer a pending interaction through the authenticated human door. Audience gate (after the existence/format/status guards): a question's ``audience`` identity OR any unrestricted caller (the operator can always unblock a stuck question) may answer; every OTHER restricted caller is a loud ``403``. An unaddressed question is answerable by any unrestricted caller and by no restricted caller. This gate is sound ONLY because a restricted caller can never obtain a question's callback ticket — the ticket is delivered exclusively over the configured channel (never on any read/stream frame), so the unauthenticated callback door stays the sole ticket-bearing surface and no filtered stream leaks it. # Cancel a pending interaction Source: https://docs.tai42.ai/api-reference/interactions/cancel-a-pending-interaction /openapi.json post /api/interactions/{interaction_id}/cancel Cancel a pending interaction — WITHDRAW one specific ask without answering or deleting its thread. The mirror of ``answer_interaction`` for the terminal-without-an-answer case: it takes the pending question's whole run down through the status-gated kill seam (``kill_park``) — every open ask of the run, nested runs and the callers waiting on it — so NO continuation fires — nothing of the run is resumed — and the removed event rides tagged ``reason="cancelled"`` so a live operator surface tells a deliberate withdrawal apart from a timeout/expiry removal. A door that started the run is delivered the ``withdrawn`` terminal: nothing is said to the person. Status-gated exactly like the answer door: only a PENDING (or parked) question cancels. A question already ``answered`` (its state retained) is a loud ``409`` conflict; a question whose state is GONE — expired, already cancelled, or never existed (all leave no distinguishable tombstone, the same limit the answer door has) — is a ``404``. Idempotent at the store seam: the teardown re-run on a withdrawn question is a clean no-op, so a re-cancel simply reports the question gone (``404``) rather than double-tearing anything down. It is a TEARDOWN door, so unlike the user-facing answer door it accepts a CALLER ask (``to="caller"``) as well as a user ask, and unlike the answer door it is answer-format-AGNOSTIC: an EXTERNAL ask is a pending ask an operator may withdraw, so it is cancellable too. The teardown withdraws the channel delivery through the delivering channel's ``withdraw`` member, so the channel's hold on the person's address is released and that address is free for the next ask when this door returns; the rest of the run's takedown is durable and finishes behind the door. The question already sent stays on the medium: a participant who still answers it reaches the callback door, finds the state gone and gets a ``404``, which the inbound ladder maps to a fresh bridged turn. Audience gate identical to ``answer_interaction`` (after the existence/status guards): a question's ``audience`` identity OR any unrestricted caller (the operator can always withdraw a stuck question) may cancel; every other restricted caller is a loud ``403``. # External interaction callback door Source: https://docs.tai42.ai/api-reference/interactions/external-interaction-callback-door /openapi.json get /api/interactions/callback/{ticket} Serve the external interaction callback door (GET page, POST answer claim). # External interaction callback door Source: https://docs.tai42.ai/api-reference/interactions/external-interaction-callback-door-1 /openapi.json post /api/interactions/callback/{ticket} Serve the external interaction callback door (GET page, POST answer claim). # External interaction mid-form reaction door Source: https://docs.tai42.ai/api-reference/interactions/external-interaction-mid-form-reaction-door /openapi.json post /api/interactions/callback/{ticket}/react Serve the unauthenticated mid-form reaction door (POST only), under the ticket capability. # List parked (async) interactions awaiting an answer Source: https://docs.tai42.ai/api-reference/interactions/list-parked-async-interactions-awaiting-an-answer /openapi.json get /api/interactions/pending A read-only admin audit of the parked async asks awaiting an answer. The native surface a scheduled watchdog flow reads to spot parks nearing (or past) their expiry. An UNRESTRICTED operator sees the whole cross-audience set — exactly the reach the operator inbox (``list_interactions``) already grants. A RESTRICTED caller sees ONLY the parks addressed to its identity, the same audience filter the inbox applies — never a 403: this operation is projected, and a projected route must agree with the gate for every identity it is projected to (the owned-keys e2e pins that invariant). Reads the ``pending:expiry`` index WITHOUT mutating it (no claim, no TTL change): purely an audit. ``limit`` is clamped to ``1..``:data:`MAX_PENDING_INTERACTIONS_LIMIT` (default :data:`DEFAULT_PENDING_INTERACTIONS_LIMIT`); it bounds the underlying index scan, so a restricted caller's filtered slice may hold fewer items. Returns ``{"items", "count"}`` — each item carries ``interaction_id``, ``group_id``, ``question`` (truncated), ``channel``, ``recipient``, ``audience``, ``thread_id`` (when the park carries one), ``expiry_at``, ``created_at``, ``mode``. # List pending interactions Source: https://docs.tai42.ai/api-reference/interactions/list-pending-interactions /openapi.json get /api/interactions The pending questions the inbox shows, one page at a time. The initial-load surface a client reads BEFORE applying the live stream (``GET /api/interactions/stream``). Order is the store's pending order (each group's most-recent question ``created_at``, then stream order within a group). A RESTRICTED caller sees ONLY questions addressed to its identity — the audience filter runs BEFORE paging so ``total`` is honest; an UNRESTRICTED caller sees every pending question. A ``page`` or ``page_size`` below 1, or a ``page`` above the served maximum, is a 400; a ``page_size`` above the cap is capped, never refused. The pending read reconciles phantom/abandoned questions as it goes (phantom-group prune, abandoned past-deadline prune, answered/missing skip). Returns ``{"items", "total", "page", "page_size", "next_page", "truncated"}`` — ``items`` carry the same shape as the stream's add frames, and ``truncated`` is always ``false`` (the pending index is the whole set, sliced in memory). # React to an open form interaction Source: https://docs.tai42.ai/api-reference/interactions/react-to-an-open-form-interaction /openapi.json post /api/interactions/{interaction_id}/react React to an OPEN reacting form through the AUTHENTICATED in-app door — the ``channel=None`` surface. The SIBLING of the authenticated answer door, under the SAME authenticated correlation: the same existence/format/status guards and the SAME audience gate (a restricted caller may react only to a form addressed to its identity). It then runs the ONE ``react`` chokepoint, which asserts the form is open, type-validates the partial values, runs the asker's handler under its stored identity with a deadline, validates the returned update, and returns it. STATELESS: no answer is recorded, the interaction is never resolved, the stored request is never rewritten. A closed form is a ``409``, a malformed request a ``400``, and a handler that raises or times out a loud ``502`` (never a stale value). # Serve interaction media stored by reference Source: https://docs.tai42.ai/api-reference/interactions/serve-interaction-media-stored-by-reference /openapi.json get /api/interactions/media/{media_id} Serve stored interaction media by its id: 400 malformed id, 404 miss, 503 blob provider gone. # Serve interaction media stored by reference Source: https://docs.tai42.ai/api-reference/interactions/serve-interaction-media-stored-by-reference-1 /openapi.json head /api/interactions/media/{media_id} Serve stored interaction media by its id: 400 malformed id, 404 miss, 503 blob provider gone. # Stream the interactions inbox live tail Source: https://docs.tai42.ai/api-reference/interactions/stream-the-interactions-inbox-live-tail /openapi.json get /api/interactions/stream Serve the interactions inbox live tail as an SSE ``StreamingResponse``. Refuses with a plain 501 when the store is unconfigured; otherwise resumes from the caller's ``Last-Event-ID`` (or the current tail end) and streams the live tail. # Exchange a one-time claim token for its API key Source: https://docs.tai42.ai/api-reference/login/exchange-a-one-time-claim-token-for-its-api-key /openapi.json post /api/login/claim Burn a one-time claim token and return the raw API key it carried — the public exchange leg. The public exchange leg of QR onboarding: its route is public by declaration (``authed=False``) and a pre-authentication surface (``pre_auth=True``), so a presented credential is never verified. The claim record is single-use: a used / unknown / expired token all answer the SAME 404 (no oracle distinguishing them). The handed-out key is guaranteed not-REVOKED (re-validated at exchange), NOT owner-alive — owner-death is enforced by the gate on every request. Response mirrors the ``loginResult`` wire shape: ``{"data": {"token": , "user_id": ...}}``. ``authority_changing=True`` keeps this OFF the default MCP tool surface: a credential-exchange login door is not an agent tool (it sits outside the ``/api/auth/*`` prefix, so the flag is what excludes it, not the prefix). # List available login methods Source: https://docs.tai42.ai/api-reference/login/list-available-login-methods /openapi.json get /api/login/methods Aggregate every registered accounts provider's login methods + the setup state. Its route is public by declaration (``authed=False``) and a pre-authentication surface (``pre_auth=True``): it is served without a credential, and a presented credential is never verified, so a stale one cannot lock a caller out of login. Each method is serialized with ``model_dump(exclude_none=True)`` so a ``None``-valued optional (icon/autocomplete) is OMITTED, never ``null`` (the Studio's zod schemas accept absent but reject ``null``). ``needs_setup`` is the one platform fact "no principal exists"; ``setup_login`` names what the setup door can attach (``kinds`` from a configured login-attaching provider, ``null`` when none); ``claim_fragment_key`` the declared claim-link fragment key. Provider errors propagate (loud, never a silently empty methods list). # Add or replace agents config entries and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/add-or-replace-agents-config-entries-and-hot-reload /openapi.json post /api/agents-config/entries Add ``entries`` to the manifest's ``agents`` section (``replace`` swaps colliding titles) and reload. # Add or replace MCP config entries and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/add-or-replace-mcp-config-entries-and-hot-reload /openapi.json post /api/mcp-config/entries Add ``entries`` to the manifest's ``mcp`` section (``replace`` swaps colliding titles) and reload. # Add or replace tools config entries and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/add-or-replace-tools-config-entries-and-hot-reload /openapi.json post /api/tools-config/entries Add ``entries`` to the manifest's ``tools`` section (``replace`` swaps colliding titles) and reload. # Add/remove names on the api_tools include/exclude lists and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/addremove-names-on-the-api_tools-includeexclude-lists-and-hot-reload /openapi.json post /api/api-tools Add/remove names on the ``api_tools`` include/exclude lists and hot-reload the manifest. Returns the apply response. An empty change, a duplicate add, or a missing remove is a loud 400/404. # Detach a single MCP server's tools by title Source: https://docs.tai42.ai/api-reference/manifest/detach-a-single-mcp-servers-tools-by-title /openapi.json post /api/mcp-status/{title}/deregister Detach a single MCP server's tools (by manifest title) without touching the other servers. The removal counterpart of ``reload_mcp``. Applied on this worker through the gate and broadcast to the fleet (all workers, or only ``targets``); the response embeds the per-worker fleet report. # Get the JSON schema for one MCP-config entry Source: https://docs.tai42.ai/api-reference/manifest/get-the-json-schema-for-one-mcp-config-entry /openapi.json get /api/mcp-config/schema The JSON schema for one MCP-config entry. # List MCP servers skipped by the viability check Source: https://docs.tai42.ai/api-reference/manifest/list-mcp-servers-skipped-by-the-viability-check /openapi.json get /api/mcp-status/failed List MCP servers skipped due to a failed viability check (server down or slow). Skipped at boot or last reload. Use ``reload_mcp`` to re-attach one once healthy. Each entry is ``{"title", "status", "category", "message", "http_status"}`` — the title plus the coarse status and a credential-free failure detail (``category`` one of ``auth`` / ``unreachable`` / ``error``, the redacted ``message``, and the ``http_status`` when the failure carried one). A query op rides the same fan-out primitive as a mutation: every worker's list arrives as its per-worker ``payload`` in the fleet report (this worker's list on its own self entry); ``targets`` optionally restricts the query to specific workers. # List the manifest MCP section's !ENV marker refs (names + set/unset only) Source: https://docs.tai42.ai/api-reference/manifest/list-the-manifest-mcp-sections-!env-marker-refs-names-+-setunset-only /openapi.json get /api/manifest/mcp-env-refs The ``!ENV ${VAR[:default]}`` markers carried by the manifest's MCP section. NAMES and BOOLEANS only, never values. Walks the PRESERVED manifest (markers intact) with the shared marker scan and returns one row per marker ref, in document order: ``{var, pointer, has_default, set}``. ``pointer`` is the RFC 6901 json-pointer of the leaf (``/mcp//config/...``); ``has_default`` is whether the ref carries a ``:default``; ``set`` is whether the var is present in ``os.environ`` — the SAME effective env the store flows into and the source-marker resolution + dangling refusals read, so a var supplied only by the deployment environment shows green, never a false red. Works identically for hand-written marker-bearing entries (a platform feature, not an mcp-server-kind feature). # Re-probe every failed MCP server Source: https://docs.tai42.ai/api-reference/manifest/re-probe-every-failed-mcp-server /openapi.json post /api/mcp-status/reload-failed Re-probe every MCP server currently in the failed list and attach the ones now viable. Applied on this worker through the gate and broadcast to the fleet (all workers, or only ``targets``); the response embeds the per-worker fleet report. # Read the PRESERVED manifest (markers intact) MCP section and user tools Source: https://docs.tai42.ai/api-reference/manifest/read-the-preserved-manifest-markers-intact-mcp-section-and-user-tools /openapi.json get /api/manifest/preserved Return the PRESERVED persisted manifest's MCP section + user tools, every ``!ENV`` marker intact. No secret is resolved — the source the Studio McpTab config editor reads so it can round-trip markers instead of baking a resolved secret. Same ``{mcp, user_tools}`` view as ``get_manifest`` — both serve the preserved read; this explicit ``/preserved`` door names that no-resolve contract in its path. # Read the PRESERVED manifest MCP section and user tools Source: https://docs.tai42.ai/api-reference/manifest/read-the-preserved-manifest-mcp-section-and-user-tools /openapi.json get /api/manifest Return the MCP section and user tools of the PRESERVED persisted manifest. ``!ENV ${KEY}`` markers are kept intact, so a secret leaf is its placeholder marker, NEVER the plaintext value. This door serves ONLY the preserved read: no resolved-view surface exists here, so no ``!ENV`` marker is ever materialized onto the wire. McpTab reads ``/api/manifest/preserved`` and ManifestTab renders the markers directly. # Reload a single MCP server by title Source: https://docs.tai42.ai/api-reference/manifest/reload-a-single-mcp-server-by-title /openapi.json post /api/mcp-status/{title}/reload Re-probe a single MCP server by ``title`` (unknown title → loud 404) and reattach it. Applied on this worker through the gate and broadcast to the fleet (all workers, or only ``targets``); the response embeds the per-worker fleet report. If the local re-probe raises, nothing is broadcast. # Remove one agents config entry by title and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/remove-one-agents-config-entry-by-title-and-hot-reload /openapi.json delete /api/agents-config/entries/{title} Remove the ``agents`` entry named ``title`` and hot-reload; an unknown title is a 404. # Remove one MCP config entry by title and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/remove-one-mcp-config-entry-by-title-and-hot-reload /openapi.json delete /api/mcp-config/entries/{title} Remove the ``mcp`` entry named ``title`` and hot-reload; an unknown title is a 404. # Remove one tools config entry by title and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/remove-one-tools-config-entry-by-title-and-hot-reload /openapi.json delete /api/tools-config/entries/{title} Remove the ``tools`` entry named ``title`` and hot-reload; an unknown title is a 404. # Replace the MCP config section and hot-reload Source: https://docs.tai42.ai/api-reference/manifest/replace-the-mcp-config-section-and-hot-reload /openapi.json post /api/mcp-config Replace the manifest's ``mcp`` section through the pipeline and hot-reload the fleet. The mutator edits the PRESERVED document in place, then ConfigService validates, persists, reloads locally, and broadcasts the reload to the whole fleet. A malformed entry fails validation inside the transaction (nothing persisted) and maps to a loud 400. # Replace the whole manifest, persist, and reload the fleet Source: https://docs.tai42.ai/api-reference/manifest/replace-the-whole-manifest-persist-and-reload-the-fleet /openapi.json post /api/manifest/replace Replace the WHOLE manifest fleet-wide and persist it. The posted ``manifest_text`` is the PRESERVED view (``!ENV`` markers intact); the server loads it to the preserved document and pushes it through the pipeline — validate the RESOLVED projection, persist verbatim (no secret bakes to disk), reload locally, and broadcast the reload so every worker re-reads the persisted store. The response embeds the per-worker fleet report as its ``fanout`` summary. A persisted replacement reaches the whole fleet, so there is no ``targets``. Authority-changing — the manifest governs ``api_tools`` + module loading — so it is off the default MCP surface (tier 2), projectable via an explicit ``api_tools.include``. # Snapshot the live MCP binding status Source: https://docs.tai42.ai/api-reference/manifest/snapshot-the-live-mcp-binding-status /openapi.json get /api/mcp-status A snapshot of the live MCP binding status. # Write a secret env value and its manifest !ENV marker together, then reload Source: https://docs.tai42.ai/api-reference/manifest/write-a-secret-env-value-and-its-manifest-!env-marker-together-then-reload /openapi.json post /api/mcp-config/secret-env Store a pasted secret as an env value and reference it from the manifest by a marker. The env KEY is EITHER an explicit ``key`` OR generated from ``key_hint`` — exactly one (``key | key_hint``). Writes the secret ``value`` to the env store under that key AND marks the key secret (adding it to ``TAI_ENV_SECRET_KEYS``), and writes an ``!ENV ${KEY}`` MARKER at ``manifest_pointer`` — all through the combined ``ConfigService.apply_env_and_change`` pipeline so the env write and the manifest mutate stay consistent. Partial-failure: a manifest-persist failure after the env write does NO rollback — the env write stands as an inert, re-runnable orphan and the op raises loudly. An explicit ``key`` colliding with an existing stored key holding a DIFFERENT value is a loud 400 naming the key; a generated key never shadows a registered settings ``env_var``. The pointer's HEAD segment MUST be ``mcp`` (loud 400 otherwise). The response is the ``reloadConfigResult`` shape; the resolved key is NEVER returned. A dangling ``!ENV`` / X-band refusal surfaces as a loud 400 naming the key (the shared boundary validator, same as ``POST /api/mcp-config``). An explicit ``key`` that is a recycle-class setting rolls a worker recycle as every env write does (the response's ``recycle``; this worker recycles itself after replying when the roll converged). # Get a marketplace listing's detail Source: https://docs.tai42.ai/api-reference/marketplace/get-a-marketplace-listings-detail /openapi.json get /api/marketplace/plugins/{ns}/{name} One listing's detail composed with its version rows in a single body. So the detail view (listing + the Versions card) is one request. The registry's display metadata (``display_name``/``homepage_url``/``license``/``readme_md``) survives the spread. # Get advisories for installed plugins Source: https://docs.tai42.ai/api-reference/marketplace/get-advisories-for-installed-plugins /openapi.json get /api/marketplace/advisories The advisory snapshot for the installed plugins, no older than the configured poll interval. A stale snapshot is refreshed on demand, and a refresh failure raises a loud 502 rather than serving stale data. # Install a marketplace plugin Source: https://docs.tai42.ai/api-reference/marketplace/install-a-marketplace-plugin /openapi.json post /api/marketplace/install Resolve, pip install, patch the manifest, reload, and record attribution. Nothing is pip-installed for a descriptor-only plugin; aborts and unwinds on any failure (see :meth:`Installer.install`). When the spec declares install-time env ``env`` / ``secret_keys`` satisfy the required ``!ENV`` markers or connector-env in the same combined transaction. ``route_mounts`` remaps declared route bases; ``accept_public_routes`` acknowledges public routes. The result's ``routes`` lists every route the install mounted. An install-time env change carrying a recycle-class key rolls a worker recycle (the receipt's ``reload.recycle``); when it converged this worker recycles itself after replying. # List installed marketplace plugins Source: https://docs.tai42.ai/api-reference/marketplace/list-installed-marketplace-plugins /openapi.json get /api/marketplace/installed The installed inventory. Shaped ``{"installed": [...]}``. Each installed row carries the update picture computed from the registry's version rows against the RUNNING contract: ``latest`` (newest published version, or ``null`` with none), ``update_available`` (a newer COMPATIBLE version exists — an incompatible newer version never advertises as an update), ``incompatible_newer`` (the newest such blocked version, so "an update exists but needs a newer core" is visible, or ``null``), and ``compat`` — the live ``{status, reason}`` verdict of the row's installed distribution against the running contract. A per-row not-found — the upstream listing vanished or was suspended — is row STATE, not a route failure: that row answers ``latest: null``, ``update_available: false``, ``missing_upstream: true``, so one dead listing never fails the whole inventory. A transport/garbled-upstream failure still surfaces as a 502, and a garbled LOCAL row (a stored spec that no longer validates, a malformed stored ref, an unparsable stored version) as a 500 naming the row — serving rows without their update picture would be a silent degrade of the spec'd shape. Each row also carries ``items`` — the ``{kind, name}`` of every item its stored spec provides (from LOCAL truth, never the registry). Studio joins these against the manifest's ``mcp`` entry titles to render an installer-written entry read-only. Each row also carries ``route_mounts`` — the persisted ``{item_name: base}`` mount the row is installed at (empty means every item at its declared base), so Studio seeds its update flow and renders routes at the ACTUAL mounted base. Each row also carries ``delivery`` — ``package`` for a pip-installed plugin, ``descriptor`` for a descriptor-only plugin whose stored spec names no package. # List marketplace categories Source: https://docs.tai42.ai/api-reference/marketplace/list-marketplace-categories /openapi.json get /api/marketplace/categories The registry's controlled category vocabulary — a plain array Studio renders as facet chips. # List marketplace item kinds Source: https://docs.tai42.ai/api-reference/marketplace/list-marketplace-item-kinds /openapi.json get /api/marketplace/kinds The registry's controlled item-kind vocabulary — a plain array Studio renders as facet chips. # Preview a marketplace install Source: https://docs.tai42.ai/api-reference/marketplace/preview-a-marketplace-install /openapi.json post /api/marketplace/install/preview Resolve a candidate install/update and report its routes WITHOUT changing any state. The resolved routes per item with ``route_mounts`` overrides applied, the collisions against the live registry (excluding the plugin's own routes on an update preview), the public routes requiring acceptance, and the ``new`` public rows an update has not already approved (see :meth:`Installer.preview`). # Search the marketplace Source: https://docs.tai42.ai/api-reference/marketplace/search-the-marketplace /openapi.json get /api/marketplace/search Proxy the registry's public search, forwarding the whitelisted facets. ``tags`` is multi-value end to end (the registry receives repeated ``tags`` params); every other facet is single-valued. ``None`` facets are dropped. The registry's rows ride through unchanged, so display metadata (``display_name``, ``icon_url``) is transparently forwarded. # Uninstall a marketplace plugin Source: https://docs.tai42.ai/api-reference/marketplace/uninstall-a-marketplace-plugin /openapi.json post /api/marketplace/uninstall Unpatch the manifest, reload, pip uninstall, and drop attribution. Convergent and registry-free (see :meth:`Installer.uninstall`). # Update a marketplace plugin Source: https://docs.tai42.ai/api-reference/marketplace/update-a-marketplace-plugin /openapi.json post /api/marketplace/update Resolve the target, pip upgrade, re-patch the manifest, reload, and upsert attribution. Nothing is pip-upgraded for a descriptor-only plugin; the same pre-flights apply as install (see :meth:`Installer.update`). ``env`` / ``secret_keys`` satisfy a new version's required ``!ENV`` markers or connector-env. ``route_mounts`` remaps declared route bases (surviving items keep their stored base); ``accept_public_routes`` acknowledges public routes not already approved. The result's ``routes`` lists every route the update mounted. An update-time env change carrying a recycle-class key rolls a worker recycle (the receipt's ``reload.recycle``); when it converged this worker recycles itself after replying. # Upgrade all installed marketplace plugins Source: https://docs.tai42.ai/api-reference/marketplace/upgrade-all-installed-marketplace-plugins /openapi.json post /api/marketplace/upgrade-all Move every installed plugin onto its latest COMPATIBLE version in one lock-held batch. Answers ``{"results": [{ref, outcome, detail}, ...]}``. Per-ref failures (including a ref with NO compatible version) are report entries, never a batch abort — the ONE operation-level failure is the fleet marketplace lock being held elsewhere (the retriable 503). See :meth:`Installer.upgrade_all` for the outcome vocabulary. # List internal notifications Source: https://docs.tai42.ai/api-reference/notifications/list-internal-notifications /openapi.json get /api/notifications List internal notifications, newest-first. A RESTRICTED caller reads its OWN per-identity feed (complete within its own bound — never truncated by other identities' volume, never a broadcast); an UNRESTRICTED caller reads the shared feed unchanged (the operator view). # Send a human a one-way notification Source: https://docs.tai42.ai/api-reference/notifications/send-a-human-a-one-way-notification /openapi.json post /api/notifications Send a human a one-way notification, fire-and-forget. No reply is expected and nothing blocks. With a named channel the message is sent and the call returns as soon as the medium ACCEPTED it (not that a human saw it). With ``channel`` omitted the message is recorded to the internal notifications sink the Studio inbox reads. One send attempt, no retry; every failure raises loudly, never a silent no-op: * a blank message with no media to carry it (the contract admits a blank message only for a media-only send), an unknown channel name, a blank recipient/audience, a caller-supplied ``sender_identity``, a contract-invalid ``media``/``template``/ ``options``/``schema`` combination (an empty list/dict, an over-cap value, or a mutually exclusive media+template / options+template / schema+template / schema+options), a ``schema`` with no channel (the internal sink has no submission door) or one the channel's form subset/hook refuses, or a channel's permanent refusal of the input's shape (retrying cannot succeed) → 400; * a restricted caller addressing another identity (cross-identity denial) → 403; * a channel that cannot notify, or does not advertise the ``media``/``template``/``options``/``schema`` capability the send needs → 501; * a transient channel delivery failure (the raised error's ``retryable``) → 503, carrying the medium's ``retry_after`` when it named one; * a permanent channel delivery refusal → 502. ``media`` (display media sent with the message), ``location`` (a shared map pin), ``template`` (a pre-approved out-of-window send), ``options`` (FLAT tappable options a tap of which enters the conversation), ``sections`` (the SECTIONED tappable-options alternative), ``header``/``footer`` (a media header / trailing line composing an interactive message) and ``schema`` (an ask-less form's answer schema — the channel renders the message as the form's prompt, and a submission enters the conversation as a message from the person) are OPTIONAL richer-send forms — FULL parity with the flow answer path's ``AnswerPart`` vocabulary. The contract's shared composition matrix is enforced on this request model identically: ``options`` XOR ``sections`` (one choice surface); ``schema`` excludes both; ``header``/``footer`` require a choice surface; ``template`` is the standalone out-of-window send exclusive with every other content/interactive field; ``options``/ ``sections`` may each combine with ``media``/``location`` (a contract-invalid combination is a 400). On a named channel each needs a channel that advertises the matching capability (``media``→media, ``location``→location, ``template``→template, ``options``/``sections``→ interactive, ``schema``→form; ``header``/``footer`` ride the choice surface's capability, no flag of their own) — otherwise the send is refused as a 501, never downgraded to a silent freeform send. With no channel media/location/template/options/sections/header/footer are stored on the internal inbox record and rendered there; ``schema`` alone REQUIRES a channel (a 400 without one) — a stored form nobody could submit would be a dead surface, not a notification. ``data`` (per-send prefill ``values`` and per-send ``options`` lists) and ``pages`` (the stepped layout) are the ask-less form's per-send extras — the same ``FormData``/``FormPage`` an ask carries. They ride ONLY a form send: present without ``schema`` is a 400. When a ``schema`` is present each is cross-checked against it by the same validator the ask door runs — a prefill against an unknown/ill-typed property, a per-send option list on a non-string property, or a page that omits/duplicates/misnames a property is a 400. ``form_tag`` is the caller-set opaque per-send correlation the channel carries with the form and hands back on submit. It rides ONLY a form send: present without ``schema`` is a 400. The platform bounds it (an out-of-charset or over-length tag is a 400) but never interprets it, and it is not written to the in-app feed record. ``wording`` is the per-send catalogue of the notification's platform-drawn texts, grouped by what each decorates — ``form`` (a form send's chrome), ``choice`` (a choice list's control words) and ``status`` (in-flight status copy). Each member is admitted only on the surface that draws it (a ``schema`` form, a flat ``options`` list, or a sectioned ``sections`` list); a member on a notification with no such surface is a 400. It is display content, so an ``audience``-addressed send records it on the in-app feed. ``audience`` addresses the in-app record to an identity's feed; it is honored even when a channel also delivers the message (channel push AND in-app record). ``sender_identity`` is the sending identity a channel message leaves FROM, which the conversation bridge sets on the notification it builds to answer a route; a caller may not supply it here and a set value is rejected loudly, never forwarded to the channel. Returns a short confirmation string — ``"notification sent via ''"`` for a channel send, ``"notification recorded to the internal sink"`` otherwise. # Export a run's trace as a JSON download Source: https://docs.tai42.ai/api-reference/observability/export-a-runs-trace-as-a-json-download /openapi.json get /api/observability/runs/{trace_id}/trace/export Single run's full trace as a downloadable JSON file; ``resolve=true`` resolves every span's references first. # Export runs as a CSV download Source: https://docs.tai42.ai/api-reference/observability/export-runs-as-a-csv-download /openapi.json get /api/observability/runs/export Bulk export of the filtered run list as CSV (default) or JSON. Honors the same advanced filters as the run list. Capped at ``_EXPORT_CAP`` rows; truncation is flagged in-band (never a silent loss). # Get a run span's resolved value Source: https://docs.tai42.ai/api-reference/observability/get-a-run-spans-resolved-value /openapi.json get /api/observability/runs/{trace_id}/spans/{span_id}/resolved One span field's value at ``pointer``, with every record reference on the way and inside it resolved. The pointer is applied exactly as a reference's pointer: a step that lands on a reference resolves it and continues inside its value. An absent trace or span is a 404; a reference the backend does not hold (not yet ingested, or lost), or a pointer the record does not hold, is a 502 carrying the reason. # Get a run's trace Source: https://docs.tai42.ai/api-reference/observability/get-a-runs-trace /openapi.json get /api/observability/runs/{trace_id}/trace Full detailed trace for one run via the contract's ``get_trace``. The contract's ``get_trace`` always returns a trace or raises: an absent trace is ``TraceNotFoundError`` → 404, and every other backend error propagates as a loud 500. # Get a run's trace outline Source: https://docs.tai42.ai/api-reference/observability/get-a-runs-trace-outline /openapi.json get /api/observability/runs/{trace_id}/trace/outline The span tree of one run via the contract's ``get_trace``: every span without its input and output. An absent trace is a 404 and a read the backend cannot serve a 501, exactly as ``get_run_trace``. # Get the monitoring backend's observability capabilities Source: https://docs.tai42.ai/api-reference/observability/get-the-monitoring-backends-observability-capabilities /openapi.json get /api/observability/capabilities The run-list page ceiling, sorts and sort/filter incompatibilities, and the metrics measures and dimensions. Read from the backend's pure declarations (``max_page_size``, ``list_capability``, ``metrics_capability``) in the run list's wire names; no backend query is issued. # List observability runs Source: https://docs.tai42.ai/api-reference/observability/list-observability-runs /openapi.json get /api/observability/runs Filterable run list via the contract's ``list_traces``, paged with the reader's ``limit`` / ``page``. Time range plus the neutral advanced filters (tags / status / cost / token / latency ranges) and sort. ``page_size`` is capped to the backend's declared ``max_page_size``. List- or dict-typed query params are JSON-encoded in the query string (``tags`` may be a JSON list ``["a","b"]`` or a comma-separated string). # Query observability metrics Source: https://docs.tai42.ai/api-reference/observability/query-observability-metrics /openapi.json get /api/observability/metrics Aggregate metrics over the time range via the contract's ``query_metrics``. The backend DECLARES what it serves (``metrics_capability``) before any query. A backend that does not serve the core measures fails loudly (501). The by-model panel's availability is the backend's DECLARATION that it groups by the model dimension — not a swallowed query error: when declared, the by-model query runs HARD and any failure propagates; when not declared, the panel is reported absent and no query is issued. # List the registered studio plugins Source: https://docs.tai42.ai/api-reference/plugins/list-the-registered-studio-plugins /openapi.json get /api/plugins Every registered studio plugin's manifest contents and route mounts, or a loud 500 if the registry is unbuilt. ``mounts`` is read off the committed route generation, so a remapped or dropped route item reads as it is served now. # Serve a studio plugin asset Source: https://docs.tai42.ai/api-reference/plugins/serve-a-studio-plugin-asset /openapi.json get /api/plugins/{name}/studio/{path} Serve an integrity-pinned static asset for a studio plugin, or a 404/500 error response. # Create a preset Source: https://docs.tai42.ai/api-reference/presets/create-a-preset /openapi.json post /api/presets Create a preset, atomically. The shared :func:`_create_preset_core` runs the ordered name pre-checks, validation, store write THEN register (rolling the row fully back on a register failure), one ``list_changed``, and the bus rebind fan-out. The response embeds the per-worker fleet report under ``fanout``. A preset's NAME is its identity everywhere — it IS the live tool binding, and every reference keys on it deliberately; there are no surrogate ids. # Delete a preset Source: https://docs.tai42.ai/api-reference/presets/delete-a-preset /openapi.json delete /api/presets/{name} Delete a preset. A non-conflicted record is soft-deleted and its base + branch tools torn down (one ``list_changed``); a conflicted record is removed store-side ONLY (HARD delete + drop the quarantine entry), touching no registration and firing no emit. Both branches fan the removal out on the bus and embed the per-worker fleet report under ``fanout``. # Get a preset Source: https://docs.tai42.ai/api-reference/presets/get-a-preset /openapi.json get /api/presets/{name} The store record + the active ``fixed_kwargs`` + the ``uses`` / ``used_by`` cross-references. 404 for an absent name. # Get a specific preset version Source: https://docs.tai42.ai/api-reference/presets/get-a-specific-preset-version /openapi.json get /api/presets/{name}/versions/{version} One version of a preset by its integer version number. A non-integer segment is a 400 and an unknown version a 404. # List a preset's versions Source: https://docs.tai42.ai/api-reference/presets/list-a-presets-versions /openapi.json get /api/presets/{name}/versions The full version history for a preset; 404 for an absent name. # List live references to this preset Source: https://docs.tai42.ai/api-reference/presets/list-live-references-to-this-preset /openapi.json get /api/presets/{name}/referees Every live reference a rename of this preset would strand — the SAME union the rename door blocks on. The OTHER presets whose active body composes it, plus every registered referee (platform wiring — schedules/hooks/routes/extensions/parks — and plugin holders). Exposed so the UI can preflight a rename. 404 for an unknown preset, the same existence check the rename door runs first; a referee raising propagates loudly, exactly as at the rename gate. # List presets Source: https://docs.tai42.ai/api-reference/presets/list-presets /openapi.json get /api/presets One row per store-backed record (the presets plus the ``conflicted`` quarantined ones). The population the presets management table shows. # Rename a preset Source: https://docs.tai42.ai/api-reference/presets/rename-a-preset /openapi.json post /api/presets/{name}/rename Rename a preset, ATOMIC (a preset's name IS its live tool name). Runs create's ordered name pre-checks on the NEW name, BLOCKS with a 409 listing every referee if any live reference composes the current name, binds the new tool BEFORE tearing the old one down, fires one ``list_changed``, and fans the rebind out NEW-first then the old removal. The response embeds the primary rebind (new-name) fan-out report under ``fanout``; the old-name removal stays log-only. A preset's NAME is its identity everywhere — it IS the live tool binding, and every reference (preset bodies, platform wiring, plugin holders) keys on it deliberately; rename integrity is enforced at THIS gate, and there are no surrogate ids. # Roll a preset back to a version Source: https://docs.tai42.ai/api-reference/presets/roll-a-preset-back-to-a-version /openapi.json post /api/presets/{name}/rollback Re-point the active version then reload and fan out. 409 if the record is conflicted, 404 for an absent name or version, 400 if the target version cannot bind against the current live registry. The response embeds the per-worker fleet report under ``fanout``. # Save a new preset version Source: https://docs.tai42.ai/api-reference/presets/save-a-new-preset-version /openapi.json post /api/presets/{name}/versions Save a new version (carry-forward sentinels on omitted fields) then reload and fan out. 409 if the record is conflicted, 404 for an absent name. The ``list_changed`` emit is GUARDED on a real change to the serialized wire tool OR its extension combos. The response embeds the per-worker fleet report under ``fanout``. # Set a preset version's tags Source: https://docs.tai42.ai/api-reference/presets/set-a-preset-versions-tags /openapi.json put /api/presets/{name}/versions/{version}/tags Replace one version's ``tags`` annotation. Tags are labels on an immutable version body, so this edits only the annotation and never rebinds the live tool (no reload / no fan-out). 404 for an unknown preset or version; a 501 ``NotSupportedError`` (versioning-not-configured) on a store-less deploy, exactly as the create route refuses. # Validate a preset draft (dry-run) Source: https://docs.tai42.ai/api-reference/presets/validate-a-preset-draft-dry-run /openapi.json post /api/presets/validate Report whether a preset draft would be accepted, running the SAME pre-store verdict the write route would. CREATE mode when no preset named ``name`` exists, VERSION mode when one does (mode-resolved by a store lookup). Both verdicts return 200; only a malformed body is a 400. # Load a stored resource by id, optionally rendering it Source: https://docs.tai42.ai/api-reference/resources/load-a-stored-resource-by-id-optionally-rendering-it /openapi.json get /api/resources/get Load a stored resource by its id, optionally rendering it as a template. Args: resource_id: The id (path) of the resource to load. kwargs: When omitted, the resource's loaded content is returned as-is. When provided (any dict, incl. ``{}``), text is rendered as a Jinja template with these variables; media raises loudly (``400``). Returns: The loaded (and optionally rendered) content — text or a ``MediaBlock``. A missing resource is a ``404``; a traversal-escaping id, a render of media, or broken client-supplied Jinja is a ``400``. Genuine storage/transport failures raise other types and propagate as a ``500``. # Load a stored resource by id, optionally rendering it Source: https://docs.tai42.ai/api-reference/resources/load-a-stored-resource-by-id-optionally-rendering-it-1 /openapi.json post /api/resources/get Load a stored resource by its id, optionally rendering it as a template. Args: resource_id: The id (path) of the resource to load. kwargs: When omitted, the resource's loaded content is returned as-is. When provided (any dict, incl. ``{}``), text is rendered as a Jinja template with these variables; media raises loudly (``400``). Returns: The loaded (and optionally rendered) content — text or a ``MediaBlock``. A missing resource is a ``404``; a traversal-escaping id, a render of media, or broken client-supplied Jinja is a ``400``. Genuine storage/transport failures raise other types and propagate as a ``500``. # List platform runs Source: https://docs.tai42.ai/api-reference/runs/list-platform-runs /openapi.json get /api/runs A newest-first, filterable page of the platform runs index. Enumerated from the ``run_index`` table, so runs are listable without the observability vendor. With the store OFF the honest answer is the empty page. Each item carries ``traceId`` for a deep link to the vendor trace view. # Prune the platform runs index Source: https://docs.tai42.ai/api-reference/runs/prune-the-platform-runs-index /openapi.json post /api/runs/prune Delete runs-index rows older than the configured retention window; return the window and count pruned. A no-op (nothing deleted) when the store is OFF or ``TAI_RUNS_INDEX_RETENTION_DAYS`` is unset — each reported in ``skipped``, mirroring the checkpoint-retention sweep's disabled-retention posture (unset = rows kept forever). # Get the sandbox identity and resolved policy Source: https://docs.tai42.ai/api-reference/sandbox/get-the-sandbox-identity-and-resolved-policy /openapi.json get /api/sandbox Whether a sandbox provider is registered, plus the resolved security policy and live session count. # Create a schedule Source: https://docs.tai42.ai/api-reference/schedules/create-a-schedule /openapi.json post /api/schedules Schedule a caller-named tool to run on a cadence, or once now with no cadence — a run-ANY-tool door. The caller supplies ``tool_name``, so reaching this is arbitrary-tool-execution privilege (the recurring firing runs the named tool with real side effects). As a "run any tool by name" door it is a tier-1 meta-executor, never projected to the MCP surface — matching ``run_tool`` and ``submit_run``. The four door-contract jqs make the schedule a parkable-driving door: a recurring fire evaluates them each firing over the tool's arguments with ``$parked`` bound; a RUN-ONCE shape (no cadence) honours them at the immediate create-time dispatch through the SAME door mechanism. A contract jq requires ``execution_key`` — the identity a receiver-less recurring fire binds to rebind/resume a park it raises. # Delete a schedule Source: https://docs.tai42.ai/api-reference/schedules/delete-a-schedule /openapi.json delete /api/schedules/{schedule_name} Delete the schedule named ``schedule_name``; raises 501 when no scheduling backend is present. # Get the server date and time Source: https://docs.tai42.ai/api-reference/schedules/get-the-server-date-and-time /openapi.json get /api/schedules/server-datetime Return the server's current date and time, read from the platform's own clock. # List schedules Source: https://docs.tai42.ai/api-reference/schedules/list-schedules /openapi.json get /api/schedules List the installed backend's schedules as ``ScheduleListRow`` dumps; 501 when no scheduling backend is present. The backend tool's output is validated as ``list[ScheduleListRow]``; any other shape is a 500 naming the first violation. # A state's consumers Source: https://docs.tai42.ai/api-reference/states/a-states-consumers /openapi.json get /api/states/{name}/consumers Everything that binds the state — every registered consumer family — as the Consumers tab reads it. A consumer family the deployment cannot list is a labelled, muted row. # A state's record statistics Source: https://docs.tai42.ai/api-reference/states/a-states-record-statistics /openapi.json get /api/states/{name}/stats Record counts for a state — the total and the per-subject-kind breakdown. # A subject's write audit trail Source: https://docs.tai42.ai/api-reference/states/a-subjects-write-audit-trail /openapi.json get /api/states/{name}/records/{target_kind}/{target_name}/{kind}/{key}/writes One keyset page of a subject's write ledger. ``items`` (each entry's seq, timestamp, completed origin (door/actor/consumer/meta/run/ turn) and the paths it touched) and the ``next_cursor`` the next page reads from (null on the last page). # Apply an update-purpose template_jq program to a subject's record Source: https://docs.tai42.ai/api-reference/states/apply-an-update-purpose-template_jq-program-to-a-subjects-record /openapi.json post /api/states/{name}/records/{target_kind}/{target_name}/{kind}/{key}/template-jq/{program} Apply an ``update``-purpose ``template_jq`` program to one subject. Its jq returns a template-relative op batch applied through the same chokepoint as a delta — regimes, the composing-shape guard, trace stamping and ``op_id`` idempotency all hold. ``program`` may be unqualified or ``