API reference

Every AI Support endpoint — forty-eight admin routes with the permission on each, grouped by the screen that calls them, and the eight customer routes — plus the conventions that mislead a client.

22 min readUpdated 7 August 2026api, endpoints, permissions, reference

Two surfaces.

Everything under /api/admin/ai/support carries an explicit permission and is checked twice — once by the page gate that opens the screen, once by the route itself. Everything under /api/ai/support is the customer's own side: signed in, scoped to the caller, and carrying no permission at all, because a customer holds none.

There are forty-eight admin routes and eight customer ones. Eight of the forty-eight are the admin assistant's own console, all gated on one key; they are grouped together below. The addon declares no WebSocket route of its own: the Live Inbox rides the core support socket at /api/user/support/ticket, subscribing to ticket-{id} like the desk console does.

Conventions that will catch you out

The platform's CORS middleware writes response headers before any route handler runs, and uWebSockets locks the status line at 200 OK on the first header write. So a refusal declared as 403 or 429 in the code below usually arrives as an HTTP 200 whose body is { "message": "…", "statusCode": 403 }.

The frontend's own fetch wrapper compensates by treating a 2xx body carrying statusCode >= 400 as an error. A client of your own must do the same, or a 403 on the settings write will read as a successful save.

Do not "fix" this by changing status codes: every existing consumer, including the whole admin panel, is written against 200-plus-body.

The coverage endpoint is answer-coverage/, not coverage/. That is not a naming preference. .gitignore carries a bare coverage/ — the test runner's output directory — and a bare pattern matches a directory of that name anywhere in the tree. A route file under coverage/ works perfectly on the machine that wrote it, is silently untracked, and does not exist in a fresh clone, in the build, or on any buyer's install. The route loader would report nothing and the screen would simply 404.

The catalogue routes are the source of truth, not the console. guide/, operation/ and workflow/ each return what this build ships. Only workflow/ also reports why an entry cannot currently run; guide/ and operation/ return the entries alone, with no availability or blocked-reason field on them. A static list in the admin form would be a second copy that drifts, and it would drift in the worst direction: a workflow present in the backend and absent from the screen is one the operator can never enable. console/catalogue is the same contract for the admin side, with one addition: it reports why an entry is unavailable in three separate fields, because "the operator has not ticked it", "your role does not hold the key" and "this platform is missing the addon" have three different remedies and three different people who can apply them.

A 429 has three reasons, not two. Every route that calls a model consults the budget first — every one except knowledge/search, which is the exception documented below — and the gate refuses for daily_budget, monthly_budget or unpriced_model. The third is the one a client will not expect: when a cap is set and any model call this month moved tokens and recorded no cost, the gate refuses rather than assuming those calls were free. It is the case of an install that pointed the compatible adapter at a metered endpoint and was told $0.00 every day while the real bill grew. The way out is to price the endpoint with AI_SUPPORT_OPENAI_INPUT_USD_PER_MTOK and AI_SUPPORT_OPENAI_OUTPUT_USD_PER_MTOK, or to set the caps to 0 — which has always meant "no ceiling", and is the documented escape hatch for an install whose per-token cost really is zero. Retrying will not clear it.

Settings are written changed-keys-only. See below — this one has teeth.

Admin endpoints

Master switch

Writes aiSupportEnabled and aiSupportAutonomy. Super Admin only, re-checked by role inside the route.

Body: { "enabled": true, "autonomy": "COPILOT" | "AUTO_TICKET" | "AUTO_ALL" }. enabled is required. autonomy is accepted by the schema and then ignored: the handler writes COPILOT unconditionally, so a new agent always starts in copilot and is promoted afterwards through the per-agent PUT, which is where the promotion gate lives.

It exists because both keys are in the platform's protected list, so the addon's own settings PUT refuses them — and no screen anywhere rendered them as a field. The shipped experience was an addon that could not be switched on.

It also refuses to switch on something that cannot answer: enabling with aiSupportProvider still null, or with the selected provider's credentials missing from .env, returns a 400 naming what is absent rather than producing an install that reports itself on and fails every turn.

The response reports back enabled, autonomy, provider and the two channel switches, because both channels being off is the second way to get silence.

Overview and reporting

The dashboard: spend, budget, cache health, outcomes and promotion readiness.
Handovers grouped by what would fix them, each with a meaning and a next action.
What share of recent ticket subjects your documentation answers. No AI call.

escalation takes ?days= — 1 to 365, defaulting to 30.

answer-coverage replays recent ticket subject lines through the live index and reports how many clear the retrieval floor and the operator-policy gate — the same two decisions the agent makes at answer time. It calls no model. Failures are clustered by meaning, and each cluster states what documenting it would be worth in percentage points.

The Overview page opens on access.ai.support while every figure on it comes from a view.ai.support.analytics route. A role with one and not the other gets an empty dashboard and no error — the most common permission complaint in this addon.

Live Inbox

The queue: scoped, sorted and paged in SQL.
One conversation with the reasoning rail: transcript, handovers, every AI turn, and the pending draft.
Stop the AI on this conversation and mark it yours.
Hand it back, or leave the AI off for this conversation only.
Copilot: generate a suggested reply, or send the edited text.
Cancel a running multi-step process on a ticket.
Turn a resolved conversation into a DRAFT knowledge article.

{id} on the session routes is the ticket id, not the session id.

inbox takes:

Parameter Values Default
scope open, waiting, mine, ai, closed, all open
sort priority, activity, created, importance, turns, cost, subject, customer priority
order asc, desc the sort key's own direction
page 1-based; clamped to the last page with rows 1
perPage 5–200. limit is an alias 50
search Subject, customer name or email, escalation reason, or a whole ticket id
state One session state, narrowing whatever scope selected

An unrecognised sort, order or scope falls back to the default rather than failing, and the response echoes scope, sort, order and page so a caller can tell what it was actually served.

Alongside items it returns total (rows matching this scope and search, across every page), pages, perPage, and counts — a tally per scope computed over the whole population, narrowed by search but never by scope.

Rows carry state, escalationReason, turnCount, costUsd, channel, ticketStatus, importance, createdAt, waitingSince, humanAgentId and ageMinutes — described in Two inboxes, one set of tickets.

draft takes { "action": "generate" } or { "action": "send", "text": "…", "turnId": "…" }. generate is draft-only on every install, including one configured to answer autonomously: it never appends to the ticket and never mails the customer, so the text always comes back to the composer for a person to send. Sending records how far the human moved the text, and that edit distance is the evidence the promotion gate reads — see Autonomy.

release takes keepAiOff, note and announce. takeover takes announce only; both announce by default.

workflow/{id}/cancel closes only the step the customer has not confirmed yet. Steps already completed are not undone. A note is posted into the conversation so the customer is not left with a button that silently does nothing.

Provider

Connection status for every provider, the model catalogue and prices.
Runs credentials, reachability and a real generation. Bills a fraction of a cent.
Switches which provider answers. Super Admin only, re-checked by role.

provider never returns a credential value — only a short prefix, enough to confirm which key is loaded without the key being readable.

test takes { "provider": "anthropic" | "mashdiv" | "openai_compatible" } and passes only if the generation step succeeded, because that is the step that corresponds to a customer being answered.

activate takes { "provider": "anthropic" | "openai_compatible" | "mashdiv" | "null" }, refuses a provider whose credentials are missing, and translates the model settings into the new provider's vocabulary by tier — aiSupportModel, aiSupportRerankModel and aiSupportClassifierModel all move, so a stored value cannot disagree with what actually runs.

Agents

List personas.
Create a persona.
Create one sensible default. Refuses if any agent already exists.
Update a persona.
Delete a persona.

Create requires name. The rest is optional: avatar, persona, disclosureText, model, effort, maxTokens, autonomy, toolsEnabled, channels, languages, workingHours, timezone, status.

Sources and articles

Knowledge sources and their index state.
Re-chunk the documentation packs, your articles and the FAQ, then rebuild the index.
Run retrieval for a question without calling the answering model.
List articles.
Create an article. Publishing re-chunks it immediately.
Update an article.
Delete an article.

knowledge/search takes ?q= (required) and ?rerank=false. It is the retrieval tester: it shows exactly what the agent would have been given, its confidence, and whether that clears the floor below which it refuses to answer.

The reranker is a model call. Retrieval runs it whenever the first pass finds more candidates than aiSupportTopK, which on any real corpus is every query, so the default path here bills a small generation per search.

Two things make that sharper than it sounds. The rerank parameter overrides aiSupportRerankEnabled rather than being narrowed by it, so this route reranks even on an install that has switched reranking off everywhere else. And the route checks no budget and writes no turn row — it is the one model-calling admin route with no ceiling in front of it, and its spend is invisible to the figure that caps every other one.

Pass ?rerank=false to get the BM25 ordering instead. The response shape is identical, reranked comes back false, and it genuinely costs nothing.

reindex is safe at any time — sources are replaced atomically, so retrieval never sees a half-written corpus.

Article create requires question and answer; category, productSlug, status (DRAFT or PUBLISHED) and sourceTicketId are optional. How the index is assembled is in Knowledge pipeline.

Gaps

Questions the knowledge base could not answer, ranked by demand.
The same gaps grouped by what customers meant, not by identical phrasing.
Passages that were in front of the model when customers pressed thumbs-down.
Gaps that already have a machine-written draft attached.
Draft answers for the top open gaps. Everything it writes is DRAFT.
Resolve or dismiss a gap, optionally publishing the article that closes it.

gap/draft POST takes { "limit": n }, 1 to 25, defaulting to 10. It re-retrieves each open gap against the current corpus and drafts only where the documentation now supports an answer; gaps that need your own policy are reported rather than guessed.

gap/{id} PUT takes status (OPEN, DRAFTED, RESOLVED, IGNORED), answer — which publishes an article and marks the gap resolved in one step — plus question, category and addToQuickReplies.

gap/clusters exists because the gap table dedupes on a sorted-token fingerprint, so "how do I enable 2FA" and "turn on two factor auth" are two rows of one each. Clustering groups them by meaning, so the count you rank by is the number of customers who asked. Nothing is merged in the database; it is a view.

Settings and the Actions console

Settings, defaults, which keys are locked, who is asking, budget and cache health.
Write settings. Allowlisted, protected-key checked, and validated.
Every action the assistant may offer, with its consent text and step-up flag.
Every multi-step process this build ships, with its steps and why it cannot run.
Every walkthrough this build ships, with each stop spelled out.
Dry-run the failed-transaction sweep. Writes nothing, sends nothing, calls no model.

The GET returns settings, defaults, protectedKeys, isSuperAdmin, budget and cache. isSuperAdmin is a hint for rendering, not the gate — the PUT resolves the caller's role itself on every request. On a MashDiv install the three model ids are masked to their tier in both settings and defaults, because the tier is the product.

The Settings screen submits only the fields whose value actually differs from what is stored. That is not a nicety: the backend treats every submitted key as a write request, and a resubmitted-but-unchanged protected key is refused exactly like a changed one.

The route collects every refused key and throws one 403 for the entire batch. It does not drop the offender and save the rest. So a client that helpfully posts the whole settings object back will fail on every request, and an admin who changes the retention period and touches a protected key loses the retention period too.

Two different refusals, worded differently on purpose:

  • "These settings cannot be changed here" — the key is outside this addon's allowlist, or it is another addon's protected key. No role accepts it here.
  • "Only a Super Admin can change these" — one of this addon's own protected keys, and the caller's role is not Super Admin. The message says so, and adds that nothing was half-applied.

Keys outside the allowlist are refused, never silently dropped — silently dropping is how an operator ends up believing they changed something they did not.

Two fields are validated at write time and nowhere else, because this is the only moment a human is present to be told: aiSupportOfficeHours must parse (one line per day, mon 09:00-17:00, closed for a shut day) and must not be closed on every day; aiSupportOfficeHoursTimezone must be an IANA name the server recognises, so a typo is rejected instead of silently becoming UTC.

A successful write invalidates the settings reader, the cached install profile, the route catalogue and the platform settings cache. Editing the settings table directly reaches none of them.

workflow returns a catalogue-wide blocked"provider" when the active provider cannot act (actions are gateway-only), "disabled" when aiSupportOperationsEnabled is off, null otherwise — and per entry an installed flag derived from the enabled-extensions list, its requires, its steps, and a store link when the addon it needs is genuinely absent. Unavailable entries are reported, not hidden: a row you cannot see is indistinguishable from a build that does not ship it.

operation returns key, label, description — the exact sentence the customer reads before confirming — and requiresStepUp, which is false on all three today and is surfaced because the customer-side confirm route refuses a true with a 501 rather than issuing a challenge.

guide returns every guide including the ones currently switched off, with each stop's anchor, title and body, because the words are describing your product and an allowlist you cannot see everything in is one you cannot enable anything from.

Handbook

Ask the assistant how to operate the platform, answered from the operator documentation.

Body { "question": "…" }, up to 1,000 characters. It reads no customer account data and has no tools. With no passages retrieved it returns a refusal rather than an ungrounded answer, and grounded: false says so.

aiSupportAccountToolsEnabled is deliberately not checked here — that switch is docs-only mode, and this route uses no account data under any configuration.

The admin assistant's console

Eight routes, and every one of them declares access.ai.support and nothing else. That key is the door, not the room: each catalogue behind it is filtered through the calling administrator's own permission set, so a role holding this key alone gets an assistant that answers from the operator documentation, can point at exactly one screen — the assistant's own Overview, the only entry in the 120-screen catalogue gated on this key — counts no queue and offers no action. The strongest thing the console can do is exactly what the caller could already have done by clicking.

Ask the operator assistant. Answers from the operator documentation and the platform's live state, and may offer a screen, an action or a procedure.
What is outstanding for the signed-in administrator, and whether the assistant is switched on at all.
Every action, procedure and screen the assistant could offer, and which of them are switched on for this caller.
Approve something the assistant offered. The only place an admin action executes.
Dismiss a proposal without running it. A procedure is dismissed whole.
My own conversations, newest first, plus what the other assistant surfaces have spent.
One of my conversations, every turn of it.
Delete one of my conversations. The content goes; the spend stays counted.

Asking

console POST takes { "question": "…", "sessionId": "…", "screen": "…" }. Only question is required, and it is capped at 1,000 characters.

sessionId continues a conversation. An id that is unknown, or belongs to another administrator, starts a fresh one rather than returning an error — telling a caller whether a session id exists would be an oracle over somebody else's history. The last six exchanges are replayed into the prompt, truncated, because every replayed turn is input tokens on this call and on all of the ones after it.

screen is the admin path the operator is looking at. It is advisory and heavily bounded: it is matched against the admin route catalogue and dropped unless this administrator could open that screen, so a path from a request body can never put arbitrary text in front of the model.

The response is sessionId, answer, grounded, costUsd, sources, screensAvailable — a count of the catalogued screens this caller can be sent to, not the list — and at most one each of screen, action and procedure. Those three are the assistant's proposals, and they are lifted straight out of the catalogue rather than parsed back out of the answer: what the approval card renders is a constant, and the model's prose sits beside it without being able to alter it.

400 when the addon is off, and a distinct 400 when aiSupportAdminAssistantEnabled is off. 429 when the budget gate refuses. 502 when the model returns nothing.

The tool loop runs at most three rounds per question and the budget is checked once, at the top of the request — so a single question can bill more than one generation before any ceiling is consulted again.

The rail

console/pending is polled on every admin page, so it is audit: false and it always returns the whole shape: { enabled, action, procedure, overview, inbox }.

enabled is explicit and is the first thing to read. It is false when the addon or the admin assistant is switched off, and the rail is expected to draw nothing at all rather than an Ask box whose every answer is a refusal.

inbox is { open, waiting, mine, screen, title } — counts, never rows — and it is null, not zero, for an administrator who cannot open the Live Inbox. A zero would be a claim about the platform; null is "not yours to know". overview is null on the same rule.

procedure carries a receipt as well as a running step. For ten minutes after a procedure's last step completes, the same envelope comes back with state: "COMPLETED", every entry in steps marked done, and — this is the discriminator — step: null.

Branch on step, not on procedure. A client that reads any non-null procedure as work waiting for approval will draw an approval card with no proposal id behind it for ten minutes after every success.

The shape exists because the alternative is worse: answering only "is there a PROPOSED row?" makes the panel that followed the administrator across five screens vanish at the exact moment the procedure succeeded, taking with it the only thing that said so.

A standalone proposal stops being offered 30 minutes after it was made; a procedure is aged by its first step and stops after 24 hours, because a step's own row is written when the previous one completes. Both are re-derived through the live catalogue on every poll, so an action the operator has since withdrawn from the allowlist, or one this administrator has lost the permission for, disappears from the rail instead of sitting there until it is pressed.

The catalogue

console/catalogue returns demo, trialUrl, enabled, actionsEnabled, canConfigure, actions, procedures and screens.

An action carries allowed (the operator ticked it) and permitted (this role holds the key). A procedure carries permitted — true only when every step is — blocked (the platform is missing the addon that procedure needs) and offerable, which is all of those together plus the admin assistant being switched on. They are separate fields because each kind of "no" has a different remedy and a different person who can apply it, and a single boolean produces a screen that says "off" next to something the reader cannot switch on. Each step repeats its own permission, permitted and actionDisabled, so the screen can say which step is the problem rather than only that there is one.

screens is { total, reachable, byArea }, and byArea lists only the screens this administrator may actually be sent to. Naming the rest would turn the response into an enumeration of the whole admin surface, which is the thing the per-caller filter exists to prevent.

Approving and dismissing

{id} on both console/action routes is the proposal id from the assistant's card. Neither takes a body.

The PUT is the only place an admin action ever executes. propose_admin_action writes a row in PROPOSED and returns a description — that is the model's entire capability, and no runner is reachable from a tool call. The permission is re-derived from the live catalogue and checked against the approving session; the key snapshotted on the row is for the audit trail only, so re-gating an action to a stricter key takes effect on proposals written before the change. approvedBy and approvedAt are written in the same conditional UPDATE that moves the row out of PROPOSED, which is what makes the audit claim a query rather than an argument: no row reaches COMPLETED or FAILED without naming who approved it.

It returns { message, state }, plus procedure when the approved row was a step — carrying the next step, or a terminal COMPLETED, FAILED or CANCELLED. A failed step stops the procedure rather than skipping it. 403 when the caller's role does not hold the action's own key, 410 when the proposal has expired or the action is no longer in the catalogue, 404 when it was offered to somebody else. A second press returns the first press's result rather than running anything twice.

The DELETE requires nothing beyond the console key, deliberately: approving runs something, declining runs nothing, and demanding a permission to decline would strand a proposal in the panel of the very administrator who has decided against it. It writes EXPIRED, only over a PROPOSED row, so it can never withdraw a decision already taken — and when the proposal is a step, every other step of the procedure still outstanding goes with it, so the next poll cannot resurrect the card or report the abandoned procedure as a success. Returns { message, dismissed }; dismissed is false when the row had already been handled, which is reported as success because the caller's intent is satisfied either way.

History

console/session takes ?offset= and pages 30 at a time, returning { sessions, hasMore, offset, spend, spendTotalUsd }.

spend is sent on the first page only and is the per-surface summary of the billed calls made outside the console — handbook questions, ask-about-a- customer, articles written from tickets, provider tests. Those are filed against a reused per-administrator ledger row in the same table, so their cost reaches the budget; they are excluded from sessions because they are not threads anybody had, and because a background call bumping their timestamp would shove the real conversation the operator came back for off the top of the page.

console/session/{id} returns { session, turns }, up to 200 turns, each with question, answer, grounded, sources, proposals, screen, costUsd and createdAt. Proposals come back so a reopened conversation reads the way it did, and the client renders them flat. Do not re-arm them: the proposal may have expired, the operator may have withdrawn the action since, and the row may already have been approved and run. console/pending is the only surface that draws a live approval control.

Both reads are scoped to the owner in the query and there is no parameter that widens it — not even for a Super Admin. Every stored answer was produced under one administrator's permission set, so a transcript is a record of one person's authorised view, and re-serving it re-serves their authority with it. Another administrator's conversation is a 404, which is also the honest answer.

console/session/{id} DELETE removes the session row and empties its turns — question, answer, sources and proposals are cleared — while each turn keeps its cost and its timestamp. Deleting the rows outright would refund their spend against the cap, and let an operator spend past their own ceiling by tidying up.

The customer record

Ask the AI a question about one named customer.

Body { "userId": "…", "question": "…" }. The one route in the addon whose subject comes from the request rather than the session. Gated on the same key as the customer record it sits on, denylisted, escaped and audited — the whole argument is on Ask the AI about this customer.

console, ask, handbook, article/from-ticket, gap/draft, provider/test and knowledge/search all call a model. The first six check the budget before they do and return 429 when a ceiling is reached; the seventh checks nothing, which is the second of the two gaps below.

The ceiling is now computed by summing ai_support_turn and ai_support_admin_turn, with no status allowlist — every row carrying a cost counts, whatever happened to it afterwards. console writes its own turn, and ask, handbook, article/from-ticket and provider/test file theirs against a per-administrator, per-surface ledger row in the same table. So an operator's own questions now spend the same allowance their customers' answers do, and the Overview figure and the gate finally describe the same population.

Two gaps are left, and both are worth knowing before you build against them:

  • gap/draft still writes no turn. It compensates with a ceiling of its own, computed for the request from the remaining daily allowance and re-checked between gaps, and it reports costUsd and stoppedOnBudget in the response — which is the only place that spend is ever visible. It reaches neither the cap nor the per-surface spend summary. Close the browser tab and the accounting goes with it.
  • knowledge/search checks no budget at all and reranks by default. See the warning under Sources and articles.

Cost and budgets has the consequences.

Customer endpoints

Eight routes under /api/ai/support. All require a session; none declares a permission, and every one is scoped to the caller. {id} is a ticket id except where noted.

The chat widget's opening line, persona, disclosure and starters. No ticket, no model call.
Whether an AI is handling this conversation, whether a person was asked for, and the persona's name.
Talk to a person: stops the AI, queues the conversation, tells the desk.
Rate one AI reply. Re-voting updates the existing vote.
An existing article for what the customer is typing. Search only, no AI call.
Record whether the suggestion answered them, or they filed anyway.
The one action or workflow step this customer still has outstanding.
Confirm an action the assistant offered. The only place an operation executes.
Route Input
human { ticketId, note? }
feedback { turnId, isHelpful, comment? }
suggest ?q= — the partly-typed subject line
suggest/{id} { outcome: "RESOLVED" | "FILED" }; {id} is the suggestion id
operation/pending ?ticketId= — optional
operation/{id} {id} is the proposal id from the assistant's message

Four of these have behaviour worth knowing before you build against them:

  • greeting creates nothing. The widget deliberately does not open a conversation when it is clicked; doing so left an empty PENDING "Live Chat" row in the desk queue for every customer who opened the bubble and closed it again. The greeting is templated, not generated.
  • greeting answers two questions, and the second one survives the first being off. The response is { enabled, greeting, quickReplies, persona, presence }. When the assistant will not speak on the live channel — the addon off, live chat off, or COPILOT, where every word the customer reads is written by a person — or when the opening line itself is switched off with aiSupportGreetingEnabled, enabled is false and the first four fields are empty. presence is resolved regardless. It is null when the operator has configured no office hours, and otherwise { open, notice, nextOpenAt, nextOpenLabel, timezone }. That is a fact about the desk, not about the assistant, and on an install with the opening line switched off it is the more useful of the two answers — so a widget that reads only enabled throws away the one thing it could still tell the customer.
  • suggest is safe on every keystroke. No model call — it is the direct-answer search plus an identity check. It ignores queries under 12 characters, is off unless aiSupportDeflectEnabled is on, and is not offered to a customer typing in a non-English language, because serving an English article verbatim to someone writing in Spanish is a confusing answer rather than a fast one. Repeated calls inside 30 minutes are deduplicated, so one deflection is not counted six times.
  • operation/{id} is the only place an action runs. The assistant cannot reach it: it may only propose, and the customer's own click executes under their own session. FILED on suggest/{id} is the more valuable of the two outcomes — it identifies an article that looks right to search and reads wrong to a person.

It always answers { operation, workflow }, and both are resolved independently — a standalone offer and a running process are different questions about different rows, and one arriving on top of the other used to declare the other finished.

workflow is not only a step waiting to be confirmed. For ten minutes after a process completes, the same envelope comes back with state: "COMPLETED", every entry in steps marked done, and step: null. That is the receipt, and step is the discriminator — a client that treats any non-null workflow as outstanding work will draw a confirm button with no proposal behind it for ten minutes after every success. Only COMPLETED is reported this way; a process that failed or was cancelled already said so in the transcript, in a sentence written for the specific reason.

It is a pure read on every path, including for a row that has aged past its TTL. Expiry is a write, and a GET that mutated on a page load would mean opening a ticket could retire an offer the customer was about to accept. Stale rows are simply not returned; the confirm route and the hourly sweep retire them.

Omitting ?ticketId= widens it from "what is outstanding on this conversation" to "what is outstanding for this person, anywhere" — which is what the companion rail asks, because a process whose second step is now go to the deposit page takes the customer away from the transcript. It can only narrow the result: the query is scoped on the session's own user id either way.

Permissions at a glance

Every admin route declares exactly one, and the counts add up to forty-eight.

Key Routes Which
access.ai.support 9 activate, and all eight console routes
access.ai.support.provider 3 provider, provider/test, provider/activate
access.ai.support.settings 6 settings GET and PUT, operation, workflow, guide, proactive/preview
view.ai.support.analytics 2 overview, escalation
view.ai.support.knowledge 8 knowledge, knowledge/search, article GET, answer-coverage, and the four gap reads
create.ai.support.knowledge 3 article POST, article/from-ticket, gap/draft POST
edit.ai.support.knowledge 3 article/{id} PUT, gap/{id} PUT, knowledge/reindex
delete.ai.support.knowledge 1 article/{id} DELETE
view / create / edit / delete.ai.support.agent 5 the five agent routes; create covers both POST and agent/default
view.ai.support.handbook 1 handbook
view.support.ticket 2 inbox, session/{id}
edit.support.ticket 4 takeover, release, draft, workflow/{id}/cancel
view.user 1 ask

The first row is the one to read twice: one route switches the addon on and the other eight are the entire admin assistant. access.ai.support is the weakest key in the addon and it opens all of that, which is safe only because everything the assistant can reach is filtered a second time through the caller's other permissions — including inside console/action/{id}, which re-derives the approved action's own key from the live catalogue and refuses with a 403 if the caller does not hold it.

Which screen each key opens, the Super-Admin-only settings, and two worked role grants are in Permissions.