Permission scopes
Choose the least API-key permissions required for ingestion, investigation, response, and administration.
On this page
API-key scopes use resource:action. A key can receive only known scopes held by the member creating
it. Project scope and permission scope are both enforced: neither can widen the other.
The authenticated scope catalog at GET /api/v1/account/api-keys/scopes and Settings → Access is
authoritative. The groups below describe the current selectable catalog.
Telemetry and investigation
| Resource | Scopes |
|---|---|
| Ingestion | ingest:write |
| Traces | traces:read, traces:delete |
| Logs | logs:read, logs:delete |
| Metrics | metrics:read, metrics:delete |
| Service health | services:read |
| Errors and Issues | errors:read, errors:write, errors:delete |
| Query and search | query:read, query:write, search:read |
| Saved searches | saved_search:read, saved_search:write, saved_search:delete |
| Customers and accounts | persons:read, persons:profile:read, groups:read, groups:write, groups:delete |
| AI agents and LLM cost | agents:read, agents:content:read, agents:write, llm:read |
| Events, cohorts, and flags | analytics:read, analytics:query, analytics:write, cohorts:write, cohorts:delete, flags:read, flags:write, flags:delete |
| Tracking-plan configuration | tracking_plans:read, tracking_plans:write, tracking_plans:delete |
| Saved-insight definitions | insights:read, insights:write, insights:delete |
| Anomalies | anomalies:read, anomalies:write, anomalies:delete |
| Replay | replay:read, replay:content:read, replay:write, replay:delete |
| Releases | releases:read, releases:write |
| Export | export:read, export:write, export:download (owner/admin only) |
errors:write allows Issue lifecycle actions and symbol uploads; deleting error data requires
errors:delete, which covers both units it has — deleting one Issue with its occurrences, and
erasing one customer's error occurrences across every identity they have used. Both are project-exact
and permanent, including for occurrences that arrive afterwards. errors:delete is not granted by
any of the narrower API-key presets (read-only, read-ingest, ingest-only) and must be named
explicitly — only the Full access preset, which grants the complete scope catalog, includes it.
Application SDKs use ingest:write for OTLP telemetry and captured errors, plus
analytics:write when sending diagnostic, identify, group, or screen events through the capture
surface. Session replay uses replay:write instead of ingest:write.
Tracking-plan list/get operations are available through REST, MCP and CLI; create/update/delete
use confirmed MCP actions and CLI, with session-only REST for human clients. See Manage tracking plans. tracking_plans:read is viewer-granted; tracking_plans:write is member-granted;
tracking_plans:delete is owner/admin-only. Writes and deletion also require the read scope for
configuration receipts. Capture and settings permissions do not grant plan management.
OAuth-delegated agents receive the read scope only; writes require a deliberately scoped API key.
Tracking permissions do not grant access to captured event values or change content permissions.
analytics:query authorizes typed analytics execution separately from reading events or results.
It also requires analytics:read and every source permission used by the definition. It is granted
to organization roles including viewers and is eligible for delegated OAuth; it does not grant
cohort mutation, export, or access to otherwise restricted source content.
Saved-insight get/list/search require insights:read. MCP and CLI create/update also require
insights:write; retirement requires admin-granted insights:delete. All mutation operations
require both mcp:read and mcp:write, preview and exact-argument confirmation. API keys can retire
only their own definitions; current admin user sessions can retire project definitions. These
permissions do not grant telemetry access. See Manage saved insights.
Derived audience creation/replacement additionally requires analytics:read, persons:read and
all inherited source permissions alongside cohorts:write. MCP and CLI mutations use both
mcp:read and mcp:write, preview/confirmation and a stable UUID key. Exact audience reads need
analytics:read, persons:read, inherited source permissions and mcp:read, but no write or query
scope. See derived audiences.
Creating, updating, refreshing, and replacing ordinary cohort membership requires cohorts:write.
This member-granted scope is separate from SDK capture (analytics:write) and is excluded from
external OAuth delegation. Application and browser SDK key presets do not grant cohort management. Deleting one is deliberately
separate under cohorts:delete, an owner/admin-only scope, because deletion is destructive.
services:read names per-service request rate, error rate, and duration, plus the per-operation
breakdown. agents:read names AI agent-run summaries: which agent ran, its model, step and
tool-call counts, duration, token and cost totals, and outcome. It also names the server-owned
24-hour fleet summary and the agent inventory and evidence trail around it — declared and
telemetry-discovered agents, the review queue over
those discoveries, which telemetry belongs to which agent, step-level agent events, action receipts,
chain verification, and signed bills of materials. None of that admits prompt, completion or tool
content: agent events expose bounded identity and provenance metadata, never content or a content
digest. It also reads exact outbound-drive audit records and recorded-session replay results. Replay
counts, stop reasons, comparison kinds, sizes, and digests remain visible; the real user turns and
agent responses still require agents:content:read. llm:read names LLM spend — the
per-model summary, the cost series, and the per-customer spend leaderboard.
agents:write is the one write in that family, and it is separate from every read in it. It names
declaring what you have deployed, the configuration that deployment resolved, the bill of materials
that configuration is made of — the three facts that let Anectico tell you when a running agent has
drifted away from the composition it was approved with — and what your agent actually did: one
lifecycle step of an action it took in the world, appended to your tamper-evident action log.
It also authorizes the explicit outbound control actions: opting one registered agent into driving,
sending one caller-authored message, and explicitly replaying one real recorded session. Starting a
session replay additionally requires agents:content:read because the action exports the person's
recorded words. Because these calls can trigger another system and spend its money, agents:write
is checked even when a trusted internal caller performs the transport work.
Nothing here can be inferred from telemetry. Your spans carry a configuration hash, and only the
system that resolved the configuration can say what that hash stands for; and only your agent knows
that it intended an action, was refused one, or could not confirm that an effect landed. So a release
pipeline declares its inventory and your agent records its own conduct, both with a key holding
agents:write and no dashboard involved. Reading any of it back — a configuration, a manifest, a
receipt — is agents:read, not this scope, and so is asking for a signed checkpoint of your action
log. It is owner/admin-only to mint, because a declaration changes what drift detection watches.
The scope labels what you declare; it never lets you declare it as ours. Every artifact it produces records whose claim it is, inside the bytes the artifact's own digest covers:
| You declare | Recorded as | Anectico's own is |
|---|---|---|
| A bill of materials | authority: "declared" |
authority: "mediated" |
| An action-receipt step | receipt_authority: "reported" |
receipt_authority: "mediated" |
declared means Anectico attests to what you declared and when, not that the components are accurate.
reported means Anectico attests that it received this statement and when, not that the action
happened. mediated is reserved for what the platform itself composed or performed. You cannot choose
the value — it follows from the credential that made the call, and a request that sets one is
rejected. Because it is inside the signed or chained bytes, anyone you hand an exported record to can
tell the two apart with the published key and no call to us. (The REST authority and
receipt_authority response fields carry the same fact as the prefixed enum name —
ABOM_AUTHORITY_DECLARED/ABOM_AUTHORITY_MEDIATED and
RECEIPT_AUTHORITY_MEDIATED/RECEIPT_AUTHORITY_REPORTED — outside the signed or chained bytes.)
One action has one authority for its whole life. A step you report under an action Anectico performed is rejected rather than merged, so a receipt can never show your statement under our name.
Each of those three reads stands on its own scope. A key carrying only services:read,
agents:read, or llm:read reads that summary without also reading the spans underneath — that
separation is the point of the split. The same endpoints still accept traces:read, so a key that
already holds it keeps working unchanged; scope new keys with the specific read instead of
traces:read.
Three further scopes name recorded content rather than a summary of it, each separate from the scope that lists the same records:
| Scope | Names | Its metadata-only counterpart |
|---|---|---|
persons:profile:read |
A customer's profile properties — the traits your identify() calls attach, such as email, name, and plan |
persons:read (identity, aliases, activity timeline) |
replay:content:read |
Recorded session content: the replay snapshots and their console and network payloads | replay:read (recording list and metadata) |
agents:content:read |
Agent-run content: prompts, completions, tool arguments and results, the reasoning transcript, and exact recorded-session replay turns and responses | agents:read (run and replay metadata, comparison, and cost) |
Treat all three as personal-data scopes. Replay masking is configured in the SDK and is best-effort, and an agent transcript contains whatever your end user typed plus whatever the agent retrieved for them, so a key that reaches this content reads it in its raw form.
Each content scope is required for the content it names, everywhere. The metadata-only counterpart no longer reaches it, on any surface — not the API, not the dashboard, not the MCP endpoint, not an AI investigation. Concretely:
replay:readlists recordings and reads their start, end and duration. It does not open one: playback and the panel of errors and events captured during a session needreplay:content:read.agents:readgives a run's summary — agent, model, steps, tools, duration, tokens, cost, outcome. The run's transcript needsagents:content:read.persons:readgives the identity spine — who exists, their aliases, their activity counts and timeline. Their profile properties needpersons:profile:read, and so does anywhere those properties are used or matched: without it, an issue's affected customers and the LLM spend leaderboard identify people by their raw identifier instead of their name or email, and the people search matches identifiers only, so you cannot search by email address. (Whether a person is identified is part of the spine and is always shown — hiding it would misreport identified customers as anonymous.)
Some requests refuse outright with 403; others return the record with the restricted field
omitted, so the surfaces that carry both — the people list, an issue's affected customers, an agent
run's scores — keep working for every role.
In the dashboard, a refusal costs you that panel and nothing else. The three detail pages that carry restricted content are assembled from several reads, and the ones you are allowed keep rendering:
| Page | Still shown | Replaced by a short note |
|---|---|---|
| A customer | Their identifiers, first and last seen, the counts for events, errors, traces, logs, replay sessions and LLM spend, and the full activity timeline | The profile properties panel, and the accounts they belong to |
| An agent run | The run's shape — agent, model, status, steps, tool calls, errors, duration, cost — plus its quality scores and related errors | The span timeline, which carries the transcript |
| A session recording | Who the session belongs to, when it started, how long it ran, how many segments it has | Playback, and the product events captured during the session |
The note names what is hidden and says to ask an organization owner or admin. It never offers a retry: nothing is broken, and the same request would be refused again. An outage looks different — it says so and gives you a Retry.
The same rule follows the content wherever it is retold. An automated quality score's written
explanation is produced by a model that read the run's transcript, so it needs
agents:content:read; the numeric score and its label do not. An AI investigation is answered from
whatever its author was allowed to see, so reopening or continuing one needs the same access its
author had — otherwise Anectico shows the investigation and withholds the answer, and you can
re-run it to get one built from what you can see.
Alert labels follow it as well, and this one bites in a place people do not expect. An alert rule
grouped or filtered on a customer field labels its alerts with that field's value — an alert grouped
by user_email carries an email address. alerts:read admits the alert, not the address: the
customer-identifying labels (user_email, user_id, user_ip_address, and the customer
identifier) are returned only to a key that also holds the read scope of the signal the rule
queries, such as errors:read. Two things follow from that:
- Alerts whose labels are withheld also come back with an empty
fingerprintandgroup_key. Those are digests computed from the full label set, so returning them would let a caller confirm a guessed email address. Alerts that carry no customer-identifying label are unaffected, and grouping and de-duplication behave exactly as before either way. - Notifications never carry them. A Slack message, an email or a webhook goes to a destination
with no permissions of its own and cannot be recalled, so those labels are left out of the
delivered message and out of the stored delivery record — for every recipient, regardless of who
configured the channel. Alerts still fire, still de-duplicate and still deep-link; open the alert
in Anectico to see which customer it is about. Silences are unaffected: a silence matching on
user_emailstill matches.
Computed answers follow the data they were computed from
Anectico computes things for you — a cohort's membership, the accounts a segment matches, the customer an alert fired on, a feature-flag decision. Each of those is an answer to a question asked of your customer data, so each one needs the scope that admits the data behind it, not just the scope that admits the page it appears on.
| A key reading… | also needs |
|---|---|
| Cohort members, member counts or a cohort preview built on a profile property | persons:profile:read |
| Cohort members, member counts or a cohort preview built on events | analytics:read; add agents:content:read for event-property filters outside the exact operational-metadata allowlist |
| An account segment's matched accounts, or flag targeting on account properties | groups:read |
What you will notice:
- A flag decision never varies with something the key cannot read. Without the scope behind a cohort or an account property, that targeting condition simply does not match — the same answer an unmatched condition has always given. Give SDK keys that target on cohorts or account properties the matching scope; leave it off keys that do not.
- A member count reads as
0when the key lacks the scope, and a member list is refused. A cohort of "everyone whose email is X" has one member or none, so showing the count would be showing the answer. Zero alone therefore does not prove an empty cohort. MCP refresh receipts mark this ambiguous value as unknown and omit the count change; an authorized member-page response can establish the exact count for the returned generation. - Per-entity alerts hide which customer they are about from a key without the scope — the id,
the label, the deep link and the customer's name in the alert text all go together. The alert
itself, its severity and its value are unchanged. Cohort-derived alert identities require
analytics:read,agents:content:readandpersons:profile:read, because the alert mirror does not carry the precise cohort predicate.
One limit worth knowing: changing a key's scopes changes what it can read, not what has already been sent. The notification you configured still names the customer — you chose that destination deliberately, and a Slack message or email cannot be recalled. If a destination should stop receiving customer identities, change or remove the alert rule that feeds it; narrowing a key's scopes will not do it.
- What you wrote is still yours to read. Cohort definitions, segment predicates and cohorts you uploaded by hand are not affected — only the computed answer is.
Every role from member upward already holds all three scopes, so nothing changes for people signing in. This binds narrowly scoped API keys, and one role: a viewer can no longer list the members of a cohort built on a profile property.
A signal read scope no longer reaches customer content. It used to. Prompts, completions, tool
arguments and retrieved documents recorded as span attributes by third-party AI instrumentation
came back verbatim from the raw trace endpoints to any key holding traces:read. They do not any
more: those attributes are withheld unless the credential also holds agents:content:read, and the
response says so — the span carries an anectico.content.withheld attribute naming exactly which
keys were removed, so a missing body is never confused with a call that had none. Everything else on
the span is untouched: token counts, model names, latency, status and your own attributes are
metadata and stay readable on traces:read alone.
The same is now true of logs and of metric exemplars, and this page previously said the opposite.
A log record's attributes and resource maps, and the filtered_attributes on a metric exemplar,
carry the identical convention keys and are withheld from a credential without
agents:content:read — on log search, on a single log entry, on the surrounding-context window, on
correlation, and on the live tail. The same anectico.content.withheld marker names what was
removed. So a prompt recorded as a log attribute is now governed by the content tier wherever you
put it, and you no longer have to record model content on spans for that to be true.
Discovery and grouping follow the same rule as record reads. Log-field discovery omits a
content-classified field together with all of its sampled values. Metric-label discovery omits the
classified label; a metric query that filters or groups by that label is refused before reading its
values unless the credential holds agents:content:read and the project's current read policy
permits the class. Ordinary labels and fields are unchanged.
Two things this deliberately does not withhold. The log message itself stays readable on
logs:read: a log line is not automatically a model transcript, and treating every one as such
would make ordinary log reading impossible. And an error's extra and tags are still governed by
errors:read alone — if you put a prompt there, errors:read reads it.
One route to customer content remains open by design. If you use email addresses as customer identifiers, those identifiers are part of the identity spine and appear wherever identifiers do — on traces, logs, errors and every timeline. No scope withholds them, because they are the join key rather than a field. If a credential must not see them, identify customers by an opaque id.
An AI investigation's question and title are user-authored metadata, not derived content: they are what a person chose to type, so they are shown to anyone who can see the investigation and are governed by that choice rather than by the content tier. Treat a free-text question as visible to every role in the organization.
Scope keys accordingly: give a key the metadata-only counterpart, and add the content scope only when it genuinely needs verbatim customer content.
An AI investigation reads with your key, not with ours
An investigation is an aggregation: it gathers issues, traces, logs, customers, accounts,
incidents and LLM spend into one grounded answer. investigations:run grants the ability to ask for
one — it does not grant the data the answer is built from.
An investigation includes only evidence the key that requested it could have read directly. Each kind of evidence needs the scope that admits it on its own page:
| Evidence in an investigation | Also needs |
|---|---|
| Issue detail, occurrences, blast radius | errors:read |
| Release breakdown for an issue | releases:read |
| Traces and spans | traces:read |
| Log lines around an occurrence | logs:read |
| Cross-signal search results and trace-to-log correlation | search:read |
| Customer identity, timeline and labels | persons:read |
| Account timeline, stats and properties | groups:read |
| LLM spend summary, cost series and leaderboard | llm:read |
| Incident state, timeline and attached signals | incidents:read |
| A customer's session-recording list | replay:read |
| Agent-run summary | agents:read (or agents:content:read) |
| Agent-run steps | agents:content:read or traces:read |
| Recorded content — session playback, transcripts, profile properties | the content scopes above |
Two things follow:
- A key missing one of these gets a smaller investigation, not an error. The evidence it cannot read is not gathered — and the answer names what was left out, so a smaller investigation never passes for a complete one.
- A key that cannot read the SUBJECT is refused. Asking to investigate an issue with a key that
has no
errors:readreturns403, the same answer the issue endpoint gives.
The answer says what it did not look at. An investigation assembled without some kind of evidence
ends with a ## Not examined section naming it. That line is written by Anectico, not by the model,
so it is always there — because an answer that quietly skipped your errors must never read as "no
errors were found".
The conclusion also carries the same fact as structured evidence_disclosure data: each omission
names a closed source family and one of SCOPE, POLICY, UNAVAILABLE, or LIMIT. Use that field
for automation; the Markdown sections are for people. A present empty disclosure means nothing was
withheld, while a missing disclosure means its producer did not populate the contract. Both forms
are generated from the same omission set, so the prose and field cannot drift.
Recorded content is disclosed separately, under ## Withheld as sensitive content. The three
content scopes in the table above are asked over and above the permission to read the signal
carrying the content, so a credential can hold agents:read and receive an agent run's outcome,
latency and cost while its transcript is withheld for want of agents:content:read. That answer
looks complete, which is exactly why it gets its own section rather than sharing ## Not examined:
the run was examined, and saying otherwise would contradict evidence you can see cited in the same
answer. A credential holding the content scope gets the identical investigation it always did.
Nothing changes for people signing in: every role, including viewer, already holds every scope in the table above. This binds narrowly scoped API keys, which is what it is for. If an automation is getting thinner investigations than you expect, give its key the scopes for the evidence you want it to reason over.
Your project's content policy answers a different question, and it binds everyone. Permissions
decide who may see recorded content. The content policy decides whether
that content may be sent to a model at all — at the judge_transfer boundary — and Customer
Detective is one of the two places that boundary is crossed. Content the policy does not permit is
left out of the investigation for every caller, however broadly their key is scoped, and the answer
ends with a separate ## Withheld by policy section naming what was left out. The two sections are
deliberately separate: ## Not examined means your credential could not read something, and
re-minting the key changes it; ## Withheld by policy means your project does not allow that
content to reach a model, and only a policy change does.
Every project starts with a policy that permits what the product already did, so this changes nothing until somebody edits it. One thing is worth knowing: a content policy belongs to a project, so an investigation that spans your whole organization is asked project by project rather than once. In the organization-wide LLM spend question, each expensive agent run belongs to a single project and that project's policy decides whether the run's verbatim messages may be sent — so one answer can carry the transcripts a permitting project allows and omit those a denying project refuses, naming the omission.
The spend leaderboard is the exception, and for a different reason: it ranks people, a person is not confined to one project, and so there is no single policy to ask about their profile label. Those rows are labeled by customer id. The spend, tokens, calls and cost series are not recorded content and are unaffected.
What the viewer role can see
The three content scopes are the only reads a member holds that a viewer does not — every other difference between the two is a write, a delete, or an administrative action. Because the content scopes now bind everywhere, that difference is real:
A viewer keeps every metric, log, trace and error; dashboards, alerts, incidents and on-call; the customer and account lists, activity timelines and per-signal counts; the recording list; and agent-run summaries and LLM spend.
A viewer cannot play back a session recording or see the errors and events captured during it, open an agent run's transcript or the written explanation on its automated quality scores, read a customer's profile properties, or search customers by email. Where a name or email would have labelled a row, a viewer sees the raw identifier instead. A viewer can still run AI investigations and read their own; an investigation someone else ran with access to recorded content shows its question and not its answer.
If someone in the viewer role needs any of that, change their role to member — the scopes are not grantable separately. A key can only receive scopes its creator holds, so a key created by a viewer can never carry one of the three.
Response and collaboration
| Resource | Scopes |
|---|---|
| Alerts | alerts:read, alerts:acknowledge, alerts:resolve, alerts:rules:read, alerts:rules:write, alerts:rules:delete |
| Silences | silences:read, silences:write, silences:delete |
| Delivery channels | channels:read, channels:write, channels:delete |
| On-call | oncall:read, oncall:contacts:write, oncall:write, oncall:delete |
| Escalation | escalation:read, escalation:write, escalation:delete |
| Incidents | incidents:read, incidents:respond, incidents:write, incidents:delete |
| Dashboards | dashboard:read, dashboard:write, dashboard:delete |
| Connections and tickets | connections:read, connections:write, connections:delete, tickets:read, tickets:write, tickets:delete |
| Account segments | account_segments:read, account_segments:write, account_segments:delete |
Responder actions such as acknowledging an alert or appending to an incident are separate from configuration and deletion. Give on-call automation the responder scopes it needs, not blanket administration.
Treat channels:read as a credential-grade scope. Reading a delivery channel returns its stored
configuration as you saved it, and a webhook channel's configuration includes its URL and any
request headers you attached — which is where an authorization token usually lives. A key that only
needs to send telemetry should not carry it. Sending a test notification is channels:write, not
channels:read, because it delivers a real message to that channel's recipients; a key used for
test-sends needs both, since the test first reads the channel it is about to use.
Personal contact methods are self-service and are gated twice. oncall:read lists them and
oncall:contacts:write adds, edits, or removes them; separately, every request is confined to the
calling user's own contact methods, so no key can read or change someone else's. Both scopes are
available to every role, including viewers — being reachable when you are on a rotation does not
depend on your role. Note that an API key acts as the member who created it, so a key you did not
intend to use for this still needs oncall:contacts:write withheld to be unable to change that
member's paging destinations. Schedule administration stays on oncall:write and oncall:delete.
For dashboards, dashboard:write permits creation, cloning, and widget or layout changes. Changing
dashboard metadata or variables is limited to the dashboard owner and organization admins, and
deletion requires both dashboard:delete and dashboard-owner or organization-admin status.
dashboard:read never makes a shared URL public; dashboard links still require an authenticated
same-organization principal.
Organization, AI, and agents
| Resource | Scopes |
|---|---|
| Settings | settings:read, settings:write, settings:delete |
| GitHub configuration | github_config:read, github_config:write |
| Projects and members | projects:read, projects:write, projects:delete, members:read, members:write |
| API keys | api_key:read, api_key:write |
| Audit trail | audit:read (owner/admin only) |
| Scores and evaluations | scores:read, scores:write, evals:read, evals:write |
| Evaluation explanations | agents:content:read (additionally reveals the explanation field on an evaluation result) |
| Customer Detective | investigations:run (plus the scope for each kind of evidence an investigation should gather — see "An AI investigation reads with your key, not with ours") |
| Usage | usage:read |
| MCP | mcp:read, mcp:write |
| Documentation | docs:read |
| Agent containment | quarantine:request, quarantine:approve (owner/admin only) |
| Content governance | governance:read, governance:write (write is owner/admin only) |
Settings permissions are deliberately separate. settings:read shows stored organization and
global LLM pricing overrides without edit controls; it does not expose the embedded fallback
catalog. Adding or editing an organization override requires settings:write, and deleting one
requires settings:delete.
The same three scopes cover the organization record and its configuration entries. Reading the
organization or its configuration requires settings:read; renaming the organization or writing a
configuration entry requires settings:write; deleting the organization or removing a configuration
entry requires settings:delete. A key holding none of them authenticates normally and receives
403 PermissionDenied on all of them — which is the point: an unscoped key must not be able to
delete your organization. Changing a plan or a usage limit is not available to any organization
role, including owner.
Listing projects and listing the member roster are reads in their own right: projects:read returns
each project's name, slug, id, and retention — whether you list them all or fetch one by id — and
some endpoints need it indirectly, because exporting data validates the project you name before it
accepts the job. members:read returns the roster including pending
invitations, which are the not-yet-accepted half of the same list. Both are available to every role,
because the project selector and the member picker appear throughout the product; creating,
renaming, or deleting a project, and administering members or invitations, remain owner/admin-only
under projects:write, projects:delete, and members:write. A key without the read scopes
authenticates normally and receives 403 PermissionDenied on those endpoints, so an ingestion-only
credential cannot enumerate your projects or your people.
Key management is itself scoped. Listing the organization's keys requires api_key:read, and
creating, renaming, revoking, or re-scoping one requires api_key:write. A key without them
authenticates normally but receives 403 PermissionDenied on those endpoints — listing exposes
every key's id, name, and granted scopes, so it is not something an unscoped credential should
reach. Granting api_key:write does not widen a key: a new key can still only receive scopes its
creator already holds. Nor is it a way around a containment: a key frozen by an approved quarantine
refuses a scope change until the quarantine is lifted, and lifting takes the owner/admin
quarantine:approve. Leave both off any key that does not administer credentials.
audit:read opens the organization audit trail in Settings and through the account audit endpoint. It
can be filtered by resource, action, and result; it omits network identifiers, user agents, and
arbitrary metadata. Owners and admins receive it through their role, while narrower keys must name
it explicitly.
mcp:write is an umbrella gate. An MCP write also requires its domain scope. For example, creating
an incident requires both MCP write access and the incident permission enforced by that tool.
Exact telemetry deletion requires mcp:write plus traces:delete or logs:delete on a
project-scoped key; the MCP actions do not accept a project override.
Containing a misbehaving agent takes two people. quarantine:request lets any member or developer
ask for an asset, an observation key, or a single API key to be frozen, revoked, or both — asking
enforces nothing on its own. quarantine:approve is owner/admin-only, and it is what actually
disables the credential: approving is what signs and enforces the request, and lifting a
containment afterward takes the same scope. No key can hold only quarantine:approve — every role
that grants it also grants quarantine:request — but the platform separately refuses to let one
principal both request and approve the same containment, so a key that could do both still cannot
approve its own ask.
Withdrawing a request nobody approved is the exception, and it sits with quarantine:request:
retracting your own ask hands capability back to nobody, because a pending request never touched a
credential. Retracting somebody else's takes quarantine:approve — it suppresses a containment
before a second person can consider it. See
Revoke an agent's access for the full
request-approve-lift flow and
Withdraw a request nobody approved.
Your project's content policy
Your project's content policy says which kinds of recorded content may cross which boundary:
model transcripts, tool arguments and results, session-replay recordings, the property bag on a
person, the separate property bag on a business account or group, and agent memory — each against
being stored at all, served to a reader, included in a bulk export, or sent to an evaluation model.
governance:read returns that grid for one project or for
every project in the organization. It is available to every role, including viewers, because the grid
contains no recorded content of any kind — only the decisions and the version — and the people a
disclosure control protects should be able to check it.
governance:write replaces a project's policy and is owner/admin-only. It is the broadest single
change the API offers: widening one cell makes every transcript in that project readable,
exportable or shippable to an evaluation model, retroactively, for every key that already holds the
matching content scope. Leave it off any key that does not administer disclosure policy.
Two things are worth knowing before you rely on it. Every project already has a policy, including one you have never opened: the organization seeds each project with a grid matching what the product does today. A project whose grid has not been written yet reads back as fully denied rather than as an error — that is the shape of the response, not what the boundaries do in the meantime, which is covered below. And a change takes up to 30 seconds to take effect everywhere, so a tightening is not instantaneous.
The four boundaries are not equally reversible, and one of them is permanent. Refusing a class at the reading, exporting or evaluation boundary withholds a copy of something Anectico still holds, so widening the policy and asking again returns it. Refusing a class at the storing boundary means the content is never written down: the observation is kept — timing, status, model, token counts, cost, and the person it belongs to — and the bodies are not, for good. Widening applies forwards only and cannot recover what was recorded while the class was refused. The 30-second propagation therefore cuts both ways at that boundary: a tightening leaves up to 30 seconds of content recorded (which you can delete), and a widening leaves up to 30 seconds of content unrecorded (which you cannot recover).
At the reading boundary, "we do not know yet" means withhold — the opposite of storing. If Anectico briefly cannot read your project's policy, reads return the observation with the content left out rather than guessing that you would have allowed it. Nothing is lost: the content is still stored, and the same request returns it once the policy can be read again. The reason the two boundaries answer the same uncertainty in opposite directions is the reversibility above — a read you have to repeat costs a retry, and content disclosed against a refusal you had just recorded cannot be taken back. The evaluation boundary answers this uncertainty the same way reading does — if the policy cannot be read, nothing is sent to a model — because its destination is outside Anectico and outside your organization, which is the one place a mistaken send is worst.
A project whose policy has not been set up yet is treated as if it already had the standard one, at all four boundaries. A brand-new project gets its policy from a sweep that runs every fifteen minutes rather than at the moment you create it, so there is a short window before its grid exists. In that window the project behaves exactly as it will once the grid is written — which is the point: the alternative answers the same project two different ways depending on which side of the sweep a request lands, and records a refusal against a preference you had not expressed. Nothing is decided differently because of it; the standard posture is what your project is about to be given, and you can tighten it the moment it exists.
Exporting was the last boundary to work this way, and the change is visible on the export itself: a job decided this way is marked as having used the standard posture rather than a grid you set, so a file assembled before your project's grid existed is never mistaken for one you had approved.
An export that covers a whole organization rather than one project is the exception, and still leaves recorded content out. There is no single grid to apply — the projects it spans may each have set a different one — so nothing is assumed on their behalf. Scope the export to a project to get its content.
Reading, exporting, and automatic evaluation are independent boundaries. A class set to deny
at read and allow at export can still be exported, and the same combination with
judge_transfer can still be used by an automatic quality check. Each operation asks the cell for
the boundary it is actually crossing. Customer Detective is intentionally different: it assembles
a user-visible investigation first, so its evidence must pass read as well as the later
judge_transfer decision. Denying any one cell affects only operations that cross that boundary.
A read that covers your whole organization is governed row by row. A content policy belongs to a
project, and a request that names no project can return rows from projects whose policies disagree.
Each row is answered by the policy of the project it came from — so one organization-wide search of
traces or logs returns content from the projects that permit it and leaves it out for the projects
that refuse, in the same response, and says which rows were affected. This is what you get from the
API, the MCP endpoint and the CLI when you do not pass project_id.
Two things still come back without content, and they are fixed in different places. Trace exemplars
on a metric point carry no project of their own, so a metric read that names no project cannot obey
a policy and leaves their attributes out — name the project (project_id) and the content comes
back. And a row that carries no project at all, because the telemetry was sent with an
organization-level credential rather than a project one, has no policy that could govern it whichever
way you read it; sending that telemetry with a project-scoped credential is what fixes that one, not
the read. The dashboard always names a project for you.
At the storing boundary, "we do not know yet" means record. A project whose policy has not been set up yet, and a project whose policy Anectico briefly cannot read, both keep recording on the standard posture rather than being refused. A refusal you have already configured is not affected — the last policy read for your project stays in force through any outage of ours. The reasoning is the asymmetry above: content recorded in error can be deleted afterwards, and content never recorded cannot be brought back.
Session-replay recordings and person property bags are now enforced at the storing and reading boundaries too. Every class in the grid except agent memory now binds at every boundary where it has a flow, and the two that recently did not behave like this:
- Session replay at
store. A recording chunk is refused outright — no recording appears, and the upload is answered with an error naming the content policy rather than being accepted and thrown away. What a session was linked to is unaffected: the errors and product events captured during a session, and a recording's existence, length and person, are not the recording itself and are never withheld under this rule. - Session replay at
read. Playback is refused with an error rather than an empty timeline, because an empty timeline is indistinguishable from a session that recorded nothing. The decision follows the recording's own project, so an organization-wide credential gets the right answer without naming one. - Person properties at
store. The property values on anidentify()call are not written down; the identity itself still is. Anectico still learns who the person is, still merges an anonymous visitor into an identified one, and still links their sessions, errors and traces — only the property values are dropped, and the response says so. Clearing a property (unset) is never refused: it removes recorded content rather than recording any. - Person properties at
read. The property bag is left off a person, per person, according to the policy of the project that person belongs to — so one organization-wide list can return properties for the projects that permit them and leave them out for the projects that do not, in the same response. The record says which control removed them, because only one of the two is something you can fix with a different key.
The storing boundary also reaches the context you attach to an error. Tags, extra data and
captured request headers on an error report are your own key/value bags, so a field you name after
a model-content convention — extra.gen_ai.prompt.0.content, say — holds the same bytes the policy
governs on a span, and is treated the same way. A refusal drops those fields from the error and from
its issue, and names them in anectico.content.not_stored; every other field you sent is untouched.
Event properties are governed at BOTH boundaries. The property bag on a product event is your own key/value bag, so a property named after a model-content convention is the same bytes the policy governs on a span. At the storing boundary those properties are not written down. At the reading boundary — a customer's activity timeline, or an account's — they are left off the event and the response names them, per event, according to the policy of the project that event belongs to. So one organization-wide timeline can return properties for the projects that permit them and leave them out for the projects that do not, in the same response. Every other property you sent is untouched, and so is the event itself: its name, its time, the person and the session it belongs to.
A promoted evaluation case is refused rather than trimmed. When you promote a production agent
run into a dataset, the run's own recorded content is governed as usual — refused classes are left
out of the frozen case and the case records what went. The expected and metadata documents you
send with it are treated differently: if one of them carries a field named after a model-content
convention that your policy refuses to store, the whole promotion is refused and nothing is
written. The response names the document and the field so you can remove it and retry.
The reason is that a dataset version cannot be edited. Once written, its cases are permanent — that is what makes an evaluation result citing a version mean something. Quietly dropping a field from an expected answer would leave a permanent, unremovable specification you did not write, scoring differently for ever, and nothing in the record would say so. Refusing costs you one edit; you are still holding the document.
Reading a dataset is governed too, and separately: a case's frozen run keeps its shape with the
recorded bodies removed, while expected and metadata are withheld whole and replaced by a note
saying which field was refused and whether it was your policy or your key that refused it. That
matters because the two boundaries are separate settings — a project may record content and refuse
to hand it back — and because a policy you tighten today applies to cases promoted before you
tightened it.
Two consequences are worth knowing before you set either to deny:
- People search stops matching on property values for a project that refuses them at
read. It has to: leaving the search able to match and rank on a value it will not print would answer the question the withholding exists to refuse. Searching by any identifier you sent withidentify()keeps working. A people search that names no project takes the same narrower behavior whatever your policy says, because the search has to be built before it knows which projects the results will come from — name a project to keep property matching. - Trace and log search stops matching on withheld attribute values, for the same reason.
Free-text search keeps working over the parts it may still show you — the message, service and
operation names, and identifiers — so most searches are unaffected. Searching for an exact value
of a withheld attribute is refused with
400rather than quietly answered from a narrower set, because a filtered result you cannot tell apart from an unfiltered one is worse than an error. This applies to trace search, log search, live tail, unified search and correlation. As with people search, a request that names no project takes the narrower behavior whatever your policy says — name a project to keep attribute matching. - Property-based audiences stop refreshing for a project that refuses person properties at
read, because an audience defined by a property value is computed by reading those values. Audiences defined by behavior are unaffected.
Agent memory has no producer at all yet, so its four cells govern nothing whichever way you set
them; that is stated rather than hidden, because a control you believe in and do not have is worse
than one we do not offer. Note that documents retrieved by a RAG or search step are recorded as part
of the model transcript and are governed by that class, not by agent memory. Session replays and
person profiles are still also governed by their own permissions (replay:content:read,
persons:profile:read); the two controls compose, and neither stands in for the other.
Records and responses affected this way say so, and the three statements are deliberately different:
| Field on the record | What it means | What changes it |
|---|---|---|
anectico.content.not_stored |
The content was never written down, because your policy refused that class at the storing boundary | Nothing — this is permanent. Widening the policy applies forwards only |
anectico.content.policy_withheld |
The content exists and your project's policy refuses that class at this boundary | A change to the content policy |
anectico.content.withheld |
The content exists, your policy permits it, and this credential is not in the sensitive-content tier | A key with the matching content scope |
Where both a policy refusal and a credential gap apply to the same field, the policy is what is
reported, because a wider key would not have helped. All three fields are written by Anectico and
only by Anectico: they are stripped from anything you send — span and log attributes, product-event
properties, and error tags, extra data and captured headers alike — and re-issued from what was
actually removed, so they cannot be forged into your own telemetry. anectico.content.* is a
reserved key prefix for that reason; a key of your own with one of those three names is dropped
rather than stored, whatever your policy permits. See
Prompt and completion content.
Reading the documentation from an agent
docs:read lets a key read Anectico's own product documentation — these pages, byte for byte —
through the search_docs and get_doc actions on the MCP endpoint, and through the docs source
on search and the doc reference kind on fetch. It is the one scope on the surface that
discloses nothing about your organization: the pages it returns are published at
anectico.com/docs and are the same for everyone.
It is available to every role, including viewers, for that reason. It exists as a scope so a key can deliberately leave it out — a narrow ingest or cleanup key has no use for the manual — not because the content needs protecting. Give it to any agent you expect to configure something: an agent that can look up a scope name, an alert expression or an SDK call is an agent that stops guessing them.
Safe key patterns
- Browser or mobile SDK: project-scoped
ingest:writeplusanalytics:write; addreplay:writeonly when recording browser replay. Browser flag evaluation additionally needsflags:read; treat all flag configuration and payloads as client-visible. - Server SDK: project-scoped
ingest:write, addinganalytics:writewhen it calls identify, group, screen, or diagnostic-event methods. - OpenTelemetry collector: project-scoped
ingest:writeonly. - Any agent you expect to configure something: add
docs:readso it can look up a scope name, an alert expression or an SDK call instead of guessing one. It reads published documentation and nothing about your organization. - Read-only agent:
mcp:readand only the read domains it needs.services:read,agents:read, andllm:readare each sufficient on their own for service health, agent-run summaries, and spend — a read-only agent does not needtraces:readfor them. Addpersons:profile:read,replay:content:read, oragents:content:readonly when the agent must read customer content verbatim; withholding them is enough to withhold that content, including the prompts and completions third-party AI instrumentation records as span attributes.traces:readon its own now returns those spans with the bodies removed and ananectico.content.withheldattribute naming what was removed, so an agent can investigate latency and errors on traces without ever reaching a transcript. Your project's content policy can withhold the same bodies from a key that does hold the content scope; those responses carryanectico.content.policy_withheldinstead. - Telemetry-cleanup agent: dedicated project key with
mcp:writeand onlytraces:deleteand/orlogs:delete; both actions preview the signed project and exact target before confirmation. - CI symbol upload: dedicated project key with
errors:write; addreleases:writewhen the same job also registers a release. - Release pipeline registering agent inventory: dedicated project key with
agents:write, minted by an owner or admin. It covers the deployment, the effective configuration and the bill of materials — all three legs of one declaration. Addagents:readonly if the same job reads the registration back or checks the review queue it feeds; the write does not admit any read on its own. - Operations automation: explicit responder or configuration scopes, fixed project, no unrelated deletes.
The wildcard platform-admin permission is not grantable to API keys.
persons:correct permits an audited alias separation with a current preview revision. Granted to owners/admins only; API keys may request it explicitly. persons:read permits preview and mapping history without profile property values. SDK analytics:write does not permit corrections.