Skip to content

Guides

Give an AI Agent Scoped Access

Provision a managed agent, bind it to credentials, and let it call providers — with its own identity and audit trail.

By the end of this guide, an AI agent has its own API key, can call third-party APIs through Alter, and reaches only the credentials explicitly bound to it — not the app’s full grant store.

The flow:

  1. The operator provisions a managed agent.
  2. The operator binds credentials to the agent (a managed-secret grant directly, or a user delegates an OAuth connection to the agent).
  3. The agent process loads its own API key and calls agent.request().

In the developer portal: Agents → New Agent. Pick a name (research-bot), optionally set scope constraints and rate limits, click Create.

Equivalent SDK call:

result = await app.agents.create(name="research-bot")
print("agent id:", result.id)
print("agent key:", result.api_key) # shown ONCE
const result = await app.agents.create({ name: "research-bot" });
console.log("agent id:", result.id);
console.log("agent key:", result.apiKey); // shown ONCE

The plaintext API key is returned exactly once. Store it where the agent process will read it (a secret manager, an environment variable, the runtime’s secrets API).

Two paths, depending on whether the credential belongs to a user or the operator.

Path A — user delegates a connection. When the user runs Connect, pass agent=<agent_id>:

session = await app.create_connect_session(
allowed_providers=["google"],
user_token=user.jwt,
agent=agent_id, # consent screen will show "research-bot is requesting access"
)
const session = await app.createConnectSession({
allowedProviders: ["google"],
userToken: user.jwt,
agent: agentId,
});

On Approve, Alter writes both the connection and a delegation row binding it to the named agent.

Path B — operator issues a managed secret to the agent. From the Developer Portal: Managed Secrets → [Provider] → New Grant → Agent → research-bot (the Alter CLI’s managed-secrets grant commands support agent principals too). Agent-bound grants are an operator action: the SDK’s create_managed_secret_grant / createManagedSecretGrant methods accept an agent principal at the type level, but the backend rejects it (HTTP 422) — use the Portal or CLI. The resulting grant_id is what the agent process uses in request(grant_id=...).

The agent process loads its own key and calls request() exactly as an app would:

import asyncio, os
from alter_sdk import Agent, HttpMethod
async def main():
async with Agent(api_key=os.environ["AGENT_API_KEY"]) as agent:
# Use a managed-secret grant. The json body is forwarded to the provider
# verbatim — use the provider's wire field names (Anthropic: max_tokens).
response = await agent.request(
HttpMethod.POST,
"https://api.anthropic.com/v1/messages",
grant_id=os.environ["ANTHROPIC_GRANT_ID"],
json={
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Say hello."}],
},
)
print(response.status_code, response.json())
# Or use a delegated OAuth grant via provider+user_token, where
# user_jwt is the end user's IDP-issued JWT.
user_jwt = os.environ["USER_JWT"]
response = await agent.request(
HttpMethod.GET,
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
provider="google",
user_token=user_jwt,
)
print(response.status_code)
asyncio.run(main())
import { Agent, HttpMethod } from "@alter-ai/alter-sdk";
const agent = new Agent({ apiKey: process.env.AGENT_API_KEY! });
try {
// The json body is forwarded to the provider verbatim — use the provider's
// wire field names (Anthropic: max_tokens, not camelCase).
const response = await agent.request(
HttpMethod.POST,
"https://api.anthropic.com/v1/messages",
{
grantId: process.env.ANTHROPIC_GRANT_ID!,
json: {
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Say hello." }],
},
},
);
console.log(response.status, await response.json());
} finally {
await agent.close();
}

The audit row carries the agent identity, the user identity (if a delegation was used), and the provider.

page = await agent.list_grants()
for g in page.grants:
print(g.grant_kind, g.provider_id if g.grant_kind == "oauth" else g.managed_secret_slug)

OAuth and managed-secret grants come back in one merged response, discriminated by grant_kind.

me = await agent.me()
print(me.name, me.status, me.scopes)

me() works even if the agent is paused — useful for self-diagnostics. Any other call by a paused agent raises AgentInactiveError.

For multi-step workflows where each step should be attributed to a sub-agent:

async with researcher.trace(run_id="run_abc", role="research"):
# Every nested request() is tagged with researcher + run_abc.
await researcher.request(...)

See Agent.trace() in the Python SDK reference.

Two shapes of agent process:

  • Standalone — the agent runs in its own process / container / sandbox with only its agent key. Best for untrusted runtimes (user-installed MCP servers, sandboxed code).
  • Embedded — the agent runs inside the main app process; the app holds the agent key alongside its app key. Best for backends where adding another key boundary buys nothing.

Both shapes produce the same backend identity. The decision is purely a blast-radius question.

ErrorLikely causeFix
NoDelegatedGrantErrorThe agent has no delegation for the requested provider.Run a Connect session with agent=<this_agent_id> so a user can delegate, or issue a managed-secret grant to the agent.
AgentInactiveErrorThe operator paused the agent.Resume from the portal.
AgentRevokedErrorThe operator hard-revoked the agent.Provision a new agent.
KeyRevokedErrorThe specific API key was revoked.Mint a fresh key for the agent.
InsufficientScopeErrorThe key’s scope set does not cover the route.Mint a key with the required scopes, or update the agent’s allowlist.