Documentation Index
Fetch the complete documentation index at: https://docs.alterauth.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Every Alter API key carries an explicit list of scopes that gate which SDK methods it can call. A scope is a string like agents:read or grants:admin. The backend evaluates required-vs-granted on every call — there is no “master key” mode.
{resource}:{verb}[:{instance}]
| Part | Meaning | Example |
|---|
| resource | What the scope grants access to | agents, grants, keys, secrets, idp_users, audit_logs, usage |
| verb | What operations are allowed | read, write, admin (CRUD hierarchy) — or an action verb like tokens:retrieve |
| instance | Optional: restrict to a single resource ID | agents:write:agt_abc123 |
CRUD verb hierarchy
The three CRUD verbs are ordered. Granting a higher verb implicitly grants every lower verb on the same resource:
| Verb | What it grants |
|---|
| read | List + get |
| write | Read + create + update + delete |
| admin | Write + key lifecycle (mint / rotate / revoke / deprecate) + cross-principal operations |
So agents:write lets a key call vault.agents.list(), vault.agents.create(...), and vault.agents.delete(...). The admin verb is reserved for key lifecycle (vault.agents.mintKey(), vault.agents.rotateKey(), etc.) and cross-principal operations — not for resource deletion.
The Developer Portal labels admin as Manage in the key-creation modal — same scope string under the hood, less alarming UI.
Action verbs (orthogonal)
Some operations don’t fit a CRUD shape — they’re explicit actions. These are NOT covered by CRUD wildcards.
| Action verb | What it grants |
|---|
tokens:retrieve | Fetch a provider access token via app.get_token(...) / app.getToken(...) |
proxy:execute | Forward a request through the proxy via app.proxy_request(...) / app.proxyRequest(...) |
connect:initiate | Mint an Alter Connect session |
keys:derive | Mint a short-lived attenuated key from this one |
audit:emit | Append a row to the audit log (no read access — that’s audit_logs:read) |
Action verbs are explicit on purpose. A key with grants:admin cannot retrieve a token unless tokens:retrieve is also granted; the two are independent concerns (administering grant metadata vs accessing the credential the grant points at).
Wildcards
Three wildcard forms exist. All bind to the key’s scope catalog version at mint time — adding a new resource to the catalog later does not silently extend an existing wildcard key’s reach.
| Wildcard | Means |
|---|
* | Every CRUD resource + every action verb known at this key’s version |
*:read | Read access to every CRUD resource at this key’s version. Does NOT cover action verbs (e.g. tokens:retrieve, proxy:execute) — action verbs are not part of the CRUD grammar and a separate scope must be granted for each one. |
agents:* | Every CRUD verb (read/write/admin) on the agents resource |
* keys are powerful and require explicit org-level opt-in plus an MFA challenge at mint. They also require a CIDR allowlist by default. Use scoped keys instead unless you specifically need cross-resource access (e.g. a backup tool or a compliance scanner).
The cross-resource wildcards *:read / *:write / *:admin apply only to CRUD scopes. Action verbs like tokens:retrieve, proxy:execute, and keys:derive sit outside the CRUD hierarchy and require explicit grants — they are NEVER implied by *:read. Use * (literal universal) if you need every concrete scope at the key’s version.
Instance scoping
A scope can be pinned to a single resource ID:
This grants write on exactly one agent. It satisfies any agents:write requirement only when the call targets that specific instance — for example, vault.agents.update("agt_abc123", ...) succeeds, but vault.agents.update("agt_xyz", ...) raises InsufficientScopeError.
A resource-wide grant covers instance-scoped calls. If the key holds agents:write, a call targeting agt_abc123 succeeds because the wide grant covers the narrow one. The reverse is not true.
Per-request attenuation
A caller can narrow a key’s effective scope set for a single call via the SDK’s with_constraints / withConstraints helper:
narrowed = app.with_constraints(scopes=["grants:read"])
await narrowed.list_grants() # OK
await narrowed.agents.create(...) # raises InsufficientScopeError
The constraints are transmitted in a tamper-evident way — an upstream proxy cannot strip or rewrite them — and the backend intersects the constraint set with the key’s stored scopes. The constraint can only narrow, never broaden. A broadening attempt raises a typed validation error from the SDK.
Use this pattern when:
- A worker only needs a single capability for the next batch of work — narrow at the call boundary instead of using the full app key.
- You’re auditing a key’s behavior and want to verify what it does when constrained.
- A third-party SDK wraps Alter and wants to advertise the minimum scope it requires.
Effective scope = key ∩ constraints
The backend computes the effective scope set as:
effective = key.scopes ∩ request.constraints (if constraints sent)
= key.scopes (otherwise)
Then it checks whether the required scope is satisfied by effective. Any required scope that is not satisfied raises InsufficientScopeError from the SDK with required, granted, missing, plus scope_version and current_scope_version for version-drift diagnostics.
Scope versioning
Every key is minted at a specific catalog version. Adding a new scope to the catalog bumps the version. Existing keys keep working at their pinned version — they don’t silently gain access to scopes added later. To pick up a new scope, rotate the key.
When the missing scope exists only at the current catalog version (i.e. the key is older than the scope), InsufficientScopeError.scope_version_mismatch is True so callers can recommend a rotation rather than a scope-grant change.
Derived keys
vault.keys.derive(...) mints a short-lived attenuated key with a strict narrowing of the parent. The derived key:
- Has a subset of the parent’s scopes (permission boundary enforced at mint).
- Inherits the parent’s catalog version (no drift).
- Has its own CIDR allowlist (must be a subset of the parent’s).
- Cannot itself derive —
keys:derive is stripped at mint time.
- Has a TTL ceiling — default 24h, capped by org policy
max_derived_key_ttl_hours.
Revoking the parent cascades to every derived descendant in the same transaction. Rotation successors (rk/ak) are NOT cascade-revoked — only dk children are.
SDK method scope requirements
Use this matrix to compute the minimum scope set for a given key — a key only needs the union of scopes for the methods it actually calls.
The SDK exposes the entire surface as typed methods (e.g. vault.keys.derive()). You will not normally hit any underlying transport directly. The matrix is for picking a minimum scope set at mint time.
A call whose key does not satisfy the required scope raises InsufficientScopeError with required, granted, and missing populated on the exception.
Grants
| SDK method | Required scope |
|---|
app.list_grants() / app.listGrants() | grants:read |
app.revoke_grant(grant_id=...) / app.revokeGrant({grantId}) | grants:admin (instance-scoped on the grant) |
app.create_managed_secret_grant(...) / app.createManagedSecretGrant(...) | grants:write |
Agents
| SDK method | Required scope |
|---|
vault.agents.create(...) | agents:write |
vault.agents.list(...) | agents:read |
vault.agents.get(agent_id) | agents:read (instance-scoped) |
vault.agents.update(agent_id, ...) | agents:write (instance-scoped) |
vault.agents.delete(agent_id) | agents:write (instance-scoped) |
vault.agents.list_keys(agent_id) / vault.agents.listKeys(agentId) | keys:read |
vault.agents.mint_key(agent_id, ...) / vault.agents.mintKey(agentId, ...) | keys:admin |
vault.agents.revoke_key(agent_id, key_id) / vault.agents.revokeKey(...) | keys:admin |
vault.agents.deprecate_key(agent_id, key_id) / vault.agents.deprecateKey(...) | keys:admin |
vault.agents.undeprecate_key(agent_id, key_id) / vault.agents.undeprecateKey(...) | keys:admin |
agent.me() (agent-key only — identifies the calling agent) | (no scope — agent-key only) |
Keys (scoped-key lifecycle)
| SDK method | Required scope |
|---|
vault.keys.derive(...) | keys:derive |
vault.keys.rotate(key_id=...) | keys:admin (instance-scoped) |
vault.keys.revoke(key_id=...) | keys:admin (instance-scoped) |
Tokens
| SDK method | Required scope |
|---|
app.get_token(...) / app.getToken(...) | tokens:retrieve |
app.get_token(grant_id=...) / app.getToken({grantId}) | tokens:retrieve (instance-scoped on the grant) |
Proxied calls
| SDK method | Required scope |
|---|
app.proxy_request(...) / app.proxyRequest(...) | proxy:execute |
OAuth Connect
| SDK method | Required scope |
|---|
app.create_connect_session(...) / app.createConnectSession(...) | connect:initiate AND grants:write |
Connect needs both scopes because the session starts a connect flow (connect:initiate) and commits a grant when the user finishes (grants:write). Issuing the session without grants:write would leave the user stuck at the consent screen.
End-user auth (IDP token verification)
| SDK method | Required scope |
|---|
app.start_user_session(...) / app.startUserSession(...) | idp_users:write |
app.poll_user_session(...) / app.pollUserSession(...) | idp_users:read |
app.verify_user_token(...) / app.verifyUserToken(...) | idp_users:read |
Approvals (HITL)
| SDK method | Required scope |
|---|
app.get_approval(approval_id=...) / app.getApproval({approvalId}) | approvals:read |
app.get_approval_result(approval_id=...) / app.getApprovalResult({approvalId}) | approvals:read |
Audit log
| SDK method | Required scope |
|---|
app.emit_audit_event(...) / app.emitAuditEvent(...) | audit:emit |
audit:emit is an action verb — it is not covered by audit_logs:* wildcards. Reading the audit log is a separate scope (audit_logs:read); emitting and reading are independent permissions so a compromised key with audit:emit cannot exfiltrate audit history.
Scope catalog discovery
| SDK method | Required scope |
|---|
vault.scopes.list() | (no scope — static catalog metadata) |
The catalog is static metadata at the current catalog version. The method still requires a valid SDK client (so the env / CIDR / rate-limit gates still apply), but no scope is required to read it.
Worked examples
A read-only analytics worker that lists grants and reads audit logs:
grants:read
audit_logs:read
A connect-session minter in a webhook handler:
connect:initiate
grants:write
A token-retrieval-only backend for a single OAuth grant:
tokens:retrieve:grnt_abc123
(instance-scoped to the one grant the backend should reach)
An incident-response key for emergency key rotation:
(plus a CIDR allowlist — most orgs enforce this for keys:admin)
See also
- Audit logs — every scope decision (allow + deny) is recorded.