Skip to main content

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.

Core Concepts

vault.request() — The Authenticated HTTP Client

vault.request() acts as the authenticated HTTP client. Instead of calling httpx directly and managing auth headers, pass a grant_id and URL — the SDK makes the request with the correct credential injected automatically. Application code never sees tokens or secrets.
response = await vault.request(
    method,           # HttpMethod enum or string (e.g., HttpMethod.GET, "GET")
    url,              # Full URL of the provider API endpoint
    grant_id="...",   # Grant ID (UUID string) from Alter Connect
    json={...},       # Optional: JSON request body
    query_params={},  # Optional: URL query parameters
    path_params={},   # Optional: URL path template substitutions
    extra_headers={}, # Optional: Additional headers (Authorization is auto-injected)
    reason="...",     # Optional: Reason for audit trail
    context={},       # Optional: Per-request identity context for audit
)

Type-Safe Enums

Use enums for autocomplete and type safety:
from alter_sdk import HttpMethod

# HTTP method enums
HttpMethod.GET        # "GET"
HttpMethod.POST       # "POST"
HttpMethod.PUT        # "PUT"
HttpMethod.PATCH      # "PATCH"
HttpMethod.DELETE     # "DELETE"
HttpMethod.HEAD       # "HEAD"
HttpMethod.OPTIONS    # "OPTIONS"
Strings also work for forward compatibility:
# These are equivalent
await vault.request(HttpMethod.GET, url, grant_id=grant_id)
await vault.request("GET", url, grant_id=grant_id)

URL Path Templating

Use {placeholder} syntax for dynamic URL segments (values are automatically URL-encoded):
response = await vault.request(
    HttpMethod.GET,
    "https://api.example.com/v1/accounts/{account_id}/contacts/{contact_id}",
    grant_id=grant_id,
    path_params={"account_id": "acc-123", "contact_id": "ct-456"},
)
# → GET https://api.example.com/v1/accounts/acc-123/contacts/ct-456

Complete Examples

List Tasks

import asyncio
from alter_sdk import AlterVault, HttpMethod

async def main():
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        # Connect provider account (opens browser)
        results = await vault.connect(
            providers=["example-provider"],
        )

        # Fetch tasks
        response = await vault.request(
            HttpMethod.GET,
            "https://api.example.com/v1/tasks",
            grant_id=results[0].grant_id,
            query_params={
                "limit": "10",
                "status": "active",
                "sort": "created_at",
            },
        )
        data = response.json()

        for task in data.get("items", []):
            print(f"{task['id']}: {task.get('title', 'Untitled')}")

asyncio.run(main())

List Users

async def main():
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        results = await vault.connect(
            providers=["example-provider"],
        )

        response = await vault.request(
            HttpMethod.GET,
            "https://api.example.com/v1/users",
            grant_id=results[0].grant_id,
            query_params={"limit": "10", "sort": "name"},
        )
        users = response.json()

        for user in users.get("items", []):
            print(f"{user['name']} - {user.get('email', 'No email')}")

Create a Resource

async def create_resource(grant_id: str, name: str, description: str):
    """Use an existing grant_id (e.g., from a database) to create a resource."""
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        response = await vault.request(
            HttpMethod.POST,
            "https://api.example.com/v1/resources",
            grant_id=grant_id,
            json={"name": name, "description": description},
            reason="Creating a new resource",
        )
        data = response.json()

        if data.get("id"):
            print(f"Resource created: {data['id']}")
        else:
            print(f"Error: {data.get('error')}")

POST with JSON Body

async def create_item(grant_id: str):
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        response = await vault.request(
            HttpMethod.POST,
            "https://api.example.com/v1/items",
            grant_id=grant_id,
            json={"name": "New Item", "price": 99.99},
            reason="Creating new inventory item",
        )
        return response.json()

Using Extra Headers

Some APIs require version headers or custom metadata:
async def query_notion(grant_id: str):
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        response = await vault.request(
            HttpMethod.POST,
            "https://api.notion.com/v1/databases/{db_id}/query",
            grant_id=grant_id,
            path_params={"db_id": "abc123"},
            extra_headers={"Notion-Version": "2022-06-28"},
            json={"page_size": 10},
        )
        return response.json()

Using Managed Secrets

For APIs where the credentials are already available (API keys, service tokens), use Managed Secrets. The grant_id comes from the Developer Portal (not from an end user), and usage is identical to OAuth via vault.request():
async with AlterVault(
    api_key="alter_key_...",
    caller="my-agent",
) as vault:
    # Call your internal API — secret injected automatically
    response = await vault.request(
        HttpMethod.GET,
        "https://api.internal.com/v1/loyalty/points",
        grant_id="MANAGED_SECRET_GRANT_ID",  # from Developer Portal (not from a user)
        query_params={"user_id": "alice"},
        reason="Checking loyalty points for rewards calculation",
    )
    data = response.json()
OAuth vs Managed Secrets: For OAuth, the grant_id comes from an end user completing Alter Connect (per-user). For Managed Secrets, the grant_id comes from the Developer Portal when a credential is stored (per-service, shared across the backend). The SDK auto-detects the credential type and injects it as the correct header.
Policy enforcement, audit logging, and error handling all work identically for both OAuth and managed secrets. See the Managed Secrets guide for setup details.

Caller Tracking (AI Agents)

The caller parameter identifies this SDK instance in audit logs. This gives full observability into which agent is making API calls and why.

Why Caller Tracking?

When AI agents make OAuth-authenticated API calls on behalf of users, you need to know:
  • Which agent accessed a user’s data?
  • What was the context (tool invocation, upstream agent)?
  • When and how often did each agent make requests?
Caller tracking answers these questions automatically.

Registering a Caller

Pass caller at SDK initialization:
from alter_sdk import AlterVault

vault = AlterVault(
    api_key="alter_key_...",
    caller="email-assistant-v2",  # Optional: Unique identifier for this SDK instance
)

How Registration Works

  1. First request: SDK sends the caller string as an HTTP header
  2. Backend registers: Caller is upserted in the caller registry and assigned a UUID
  3. Backend responds: UUID returned and cached automatically
  4. Subsequent requests: Cached identifier is sent (skips database lookup)
This is fully automatic — just set caller at initialization and the SDK handles the rest.

Per-Request Context

Add execution-level context on each request using the context parameter (a freeform dict stored as JSONB in audit logs):
response = await vault.request(
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    grant_id="GRANT_ID",  # from Alter Connect
    context={"tool": "read_calendar", "agent": "cursor"},
)
Integration packages populate context automatically with framework-specific fields like run_id, thread_id, and tool_call_id. See the FastMCP integration and LangChain integration guides.

Complete Agent Example

import asyncio
from alter_sdk import AlterVault, HttpMethod

async def run_email_agent(grant_id: str):
    vault = AlterVault(
        api_key="alter_key_...",
        caller="email-assistant-v2",
    )

    # Each tool call in the agent can be tracked via context
    response = await vault.request(
        HttpMethod.GET,
        "https://gmail.googleapis.com/gmail/v1/users/me/messages",
        grant_id=grant_id,
        query_params={"maxResults": "5"},
        context={"tool": "read_emails", "agent": "email-assistant-v2"},
        reason="Reading emails for daily digest",
    )

    messages = response.json()
    print(f"Found {len(messages.get('messages', []))} emails")

    await vault.close()

asyncio.run(run_email_agent("GRANT_ID"))  # from Alter Connect

Multi-Agent Deployments

Each agent must create its own AlterVault instance with a unique caller identity. Do not share a single instance across agents — audit logs, policies, and caller registration are tied to each instance.
# Each agent gets its own vault instance
email_agent = AlterVault(
    api_key="alter_key_...",
    caller="email-assistant-v2",
)

calendar_agent = AlterVault(
    api_key="alter_key_...",
    caller="calendar-agent-v1",
)

# Audit logs and policies are tracked per caller
await email_agent.request(
    HttpMethod.GET,
    "https://gmail.googleapis.com/gmail/v1/users/me/messages",
    grant_id=gmail_grant_id,  # from Alter Connect
)
await calendar_agent.request(
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    grant_id=calendar_grant_id,  # from Alter Connect
)

# Clean up each instance
await email_agent.close()
await calendar_agent.close()
All agents can use the same API key — the caller identity (set at initialization) is what differentiates them in audit logs and policy enforcement.

Caller Tracking Parameters Reference

Initialization (set once per SDK instance):
ParameterTypeDescriptionExample
callerstrUnique identifier for this SDK instance (optional)"email-assistant-v2"
Per-request (set on each vault.request() call):
ParameterTypeDescriptionExample
contextdict[str, str]Per-request identity and execution context (audit log JSONB){"tool": "read_calendar", "agent": "cursor"}
The context dict is validated by the SDK before being sent: keys and values must be strings, the dict must have at most 20 keys, no key longer than 64 chars, no value longer than 512 chars, and the JSON-encoded payload must fit in 4 KB. Violations raise AlterValueError so a malformed context never silently disappears from the audit trail.

Grant Management

List Grants

Retrieve OAuth grants for the application:
result = await vault.list_grants()
for grant in result.grants:
    print(f"{grant.provider_id}: {grant.account_display_name} ({grant.status})")

# Filter by provider with pagination
result = await vault.list_grants(provider_id="google", limit=10)
Returns GrantListResult with grants (list[GrantInfo]), total, limit, offset, has_more. Each GrantInfo has:
FieldTypeDescription
grant_idstrUnique grant identifier (UUID). Not .id — always use .grant_id
provider_idstrProvider slug (e.g., "google", "slack")
scopeslist[str]Granted OAuth scopes
account_identifierstr | NoneProvider account email or username
account_display_namestr | NoneHuman-readable account name
statusstrConnection status (e.g., "active")
scope_mismatchboolTrue if granted scopes don’t match requested scopes
expires_atstr | NoneExpiry timestamp (if a grant policy TTL was set)
created_atstrWhen the grant was created
last_used_atstr | NoneWhen the grant was last used for an API call

Create Connect Session

Generate a URL for end-users to connect OAuth providers:
session = await vault.create_connect_session(
    allowed_providers=["google", "github"],
    return_url="https://myapp.com/callback",
)
print(f"Connect URL: {session.connect_url}")
Returns ConnectSession with session_token, connect_url, expires_in, expires_at.

Scoping grant operations to the authenticated user

In user-facing applications — where many end users share a single AlterVault instance — configure user_token_getter so every grant operation is tied to the caller’s identity. The SDK forwards the end user’s IDP JWT and the backend resolves each call against that user’s own grants.
vault = AlterVault(
    api_key="alter_key_...",
    user_token_getter=lambda: get_current_user_jwt(),
)

# list_grants() returns the caller's grants.
result = await vault.list_grants()

# revoke_grant() targets a grant the caller owns.
# Passing a grant_id that does not belong to the caller raises
# GrantNotFoundError.
await vault.revoke_grant(grant_id)

# create_connect_session() produces a Connect URL tied to the caller's identity.
session = await vault.create_connect_session(allowed_providers=["google"])

# vault.request(grant_id=...) retrieves a token only when the grant
# belongs to the caller. (Or use provider="..." for identity resolution.)
await vault.request(HttpMethod.GET, url, grant_id=grant_id)
FastAPI users get this wiring for free with AlterFastAPI — the dependency captures the incoming Bearer token and configures user_token_getter automatically. If user_token_getter is configured but cannot produce a token (e.g. the end user’s session has expired), the SDK raises AlterSDKError. Surface this as a 401 to the end user so they can re-authenticate. Omit user_token_getter for headless tools (CLIs, cron jobs, backend migrations) that operate on grants the developer manages directly. The same methods then run under the application’s own credentials.

Headless Connect (from code)

For CLI tools, Jupyter notebooks, and backend scripts, use connect() to open the browser, wait for the user to complete OAuth, and get the result back:
results = await vault.connect(
    providers=["google"],
    timeout=300,         # max wait in seconds (default: 5 min)
    open_browser=True,   # set False to print URL instead
)
for result in results:
    print(f"Connected: {result.grant_id} ({result.provider_id})")

# Now use the grant_id with vault.request()
response = await vault.request(
    HttpMethod.GET,
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    grant_id=results[0].grant_id,
)
Returns list[ConnectResult] — one per connected provider. Each has grant_id, provider_id, account_identifier, scopes, and optionally grant_policy (with expires_at if a TTL was set). Raises ConnectTimeoutError if the user doesn’t complete in time, ConnectFlowError if denied.

Grant Policy (TTL)

Use grant_policy to control how long a grant stays active. When set, the Connect UI shows an expiry picker and the grant automatically expires after the chosen duration.
# Restrict grants to a maximum of 7 days
results = await vault.connect(
    providers=["google"],
    grant_policy={
        "max_ttl_seconds": 604800,     # longest duration the user can pick
        "default_ttl_seconds": 86400,  # pre-selected in the dropdown
    },
)
The Connect UI offers these expiry options: 1 hour, 1 day, 7 days, 30 days, 90 days. Setting max_ttl_seconds hides any option that exceeds it.
ParameterDescription
max_ttl_secondsMaximum TTL the end user can select. Options above this are hidden from the picker.
default_ttl_secondsPre-selected option in the dropdown. Falls back to 1 day if not set.
The expiry picker only appears when grant_policy is passed. Without it, grants have no automatic expiry. After a grant expires, token retrieval raises GrantExpiredError and the user must re-authorize via Alter Connect.
The same parameter works with create_connect_session():
session = await vault.create_connect_session(
    allowed_providers=["google"],
    grant_policy={
        "max_ttl_seconds": 2592000,    # max 30 days
        "default_ttl_seconds": 604800, # default 7 days
    },
)

Error Handling

Always handle errors explicitly. Policy enforcement happens on EVERY token retrieval and can deny access based on configured rules.

Exception Hierarchy

All internal invariants raise typed exceptions (never bare AssertionError), so errors are always catchable with the hierarchy below.
AlterSDKError (base — also raised for input validation: invalid api_key, URL validation, missing path_params)
├── AlterValueError (SDK rejected input — fix application code, e.g. malformed `context` dict)
├── BackendError (backend errors)
│   ├── ReAuthRequiredError (user must re-authorize via Alter Connect)
│   │   ├── GrantExpiredError (403 — TTL elapsed)
│   │   ├── CredentialRevokedError (400 — auth permanently broken)
│   │   ├── GrantRevokedError (400 — grant revoked)
│   │   └── GrantDeletedError (410 — user disconnected via Wallet)
│   ├── GrantNotFoundError (404 — wrong grant_id)
│   └── PolicyViolationError (403 — policy denied access)
├── ConnectFlowError (headless connect() failed)
│   ├── ConnectDeniedError (user denied authorization)
│   ├── ConnectConfigError (OAuth app misconfigured)
│   └── ConnectTimeoutError (user didn't complete OAuth in time)
├── ProviderAPIError (provider returned 4xx/5xx)
│   └── ScopeReauthRequiredError (403 + scope mismatch - user must re-authorize)
└── NetworkError (connection/timeout failures)
    └── TimeoutError (request timed out — safe to retry)

Complete Error Handling Example

from alter_sdk import AlterVault, HttpMethod
from alter_sdk.exceptions import (
    ReAuthRequiredError,
    PolicyViolationError,
    GrantNotFoundError,
    BackendError,
    ScopeReauthRequiredError,
    NetworkError,
    ProviderAPIError,
)

async def safe_api_call(grant_id: str):
    async with AlterVault(
        api_key="alter_key_...",
        caller="my-agent",
    ) as vault:
        try:
            response = await vault.request(
                HttpMethod.GET,
                "https://www.googleapis.com/calendar/v3/calendars/primary/events",
                grant_id=grant_id,
            )
            return response.json()

        except ReAuthRequiredError:
            # Grant expired, revoked, or deleted — user must re-authorize
            print("User must re-authorize via Alter Connect")
            # Resolution: Redirect user to Alter Connect OAuth flow

        except PolicyViolationError as e:
            # Policy denied access (403) — check the reason
            print(f"Access denied by policy: {e.message} (rule: {e.policy_error})")
            print(f"Details: {e.details}")
            # Resolution: Check policy configuration in dashboard

        except GrantNotFoundError:
            # No OAuth grant for this user/provider (404)
            print("User hasn't connected Google yet")
            # Resolution: Redirect user to Alter Connect OAuth flow

        except BackendError as e:
            # Other backend errors (401 invalid API key, 500/503 backend down)
            print(f"Backend error: {e.message}")

        except NetworkError as e:
            # Connection failure or timeout (TimeoutError is a subclass)
            print(f"Network issue: {e.message}")
            # Resolution: Check network, retry with backoff

        except ScopeReauthRequiredError as e:
            # Provider returned 403 and grant has outdated scopes
            print(f"Scope mismatch on {e.grant_id}: re-auth needed")
            # Resolution: Create a new Connect session for the user

        except ProviderAPIError as e:
            # Provider API returned an error (4xx/5xx from Google, Slack, etc.)
            print(f"Provider error {e.status_code}: {e.response_body}")
            # Resolution: Check the provider's API docs

Common Error Scenarios

ExceptionHTTP StatusCauseResolution
ReAuthRequiredError400/403/410Grant expired/revoked/deletedRedirect to Alter Connect
PolicyViolationError403Policy denied accessCheck policy rules in dashboard
GrantNotFoundError404User has no OAuth grantRedirect to Alter Connect UI
BackendError401Invalid API keyCheck the api_key value
BackendError400Provider not configuredAdd provider in dashboard
BackendError500/503Backend unavailableRetry with backoff
ScopeReauthRequiredError403Provider 403 + scope mismatchUser must re-authorize via Connect
ProviderAPIError4xx/5xxProvider API errorCheck provider docs
TimeoutError-Request timed outRetry with backoff
NetworkError-Connection refusedCheck network, retry

Best Practices

# GOOD - Automatic cleanup
async with AlterVault(
    api_key="alter_key_...",
    caller="my-agent",
) as vault:
    response = await vault.request(...)
# Clients automatically closed

# BAD - Manual cleanup required
vault = AlterVault(
    api_key="alter_key_...",
    caller="my-agent",
)
response = await vault.request(...)
# Forgot to call vault.close()!
After close() is called, any further request() calls raise AlterSDKError. close() is idempotent — calling it multiple times is safe.
# GOOD - Explicit error handling
try:
    response = await vault.request(...)
except PolicyViolationError as e:
    logger.warning(f"Policy violation: {e.message} (rule: {e.policy_error})")
    return {"error": "Access denied by policy"}
except GrantNotFoundError:
    return {"error": "Please connect your account first"}

# BAD - Swallows all errors
try:
    response = await vault.request(...)
except Exception:
    pass
Store the grant_id from Alter Connect in the application database, mapped to application users.
# The app stores grant_id when the user completes Alter Connect
grant_id = db.get_grant(user_id=user_id, provider="google")
await vault.request(HttpMethod.GET, url, grant_id=grant_id)

result = await vault.list_grants(provider_id="google")
for grant in result.grants:
    print(f"{grant.grant_id}: {grant.account_display_name}")
from alter_sdk import HttpMethod

# GOOD - Type-safe with autocomplete
await vault.request(HttpMethod.GET, url, grant_id=grant_id)

# Also works - string methods
await vault.request("GET", url, grant_id=grant_id)
# GOOD - Reason shows up in audit logs
response = await vault.request(
    HttpMethod.GET, url, grant_id=grant_id,
    reason="Fetching calendar events for weekly digest",
)

Next Steps

Quickstart Guide

Full integration walkthrough with frontend + backend

Architecture

Understand the security architecture

Audit Logs

Compliance and observability features