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)
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')}")
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.
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.
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 instanceemail_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 callerawait 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 instanceawait 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.
Unique identifier for this SDK instance (optional)
"email-assistant-v2"
Per-request (set on each vault.request() call):
Parameter
Type
Description
Example
context
dict[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.
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 paginationresult = await vault.list_grants(provider_id="google", limit=10)
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.
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.
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 daysresults = 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.
Parameter
Description
max_ttl_seconds
Maximum TTL the end user can select. Options above this are hidden from the picker.
default_ttl_seconds
Pre-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 raisesGrantExpiredError 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 },)
from alter_sdk import AlterVault, HttpMethodfrom 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
# GOOD - Automatic cleanupasync with AlterVault( api_key="alter_key_...", caller="my-agent",) as vault: response = await vault.request(...)# Clients automatically closed# BAD - Manual cleanup requiredvault = 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.
Handle Errors Explicitly
# GOOD - Explicit error handlingtry: 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 errorstry: response = await vault.request(...)except Exception: pass
Store and Use Grant IDs
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 Connectgrant_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}")
Use Enums for Type Safety
from alter_sdk import HttpMethod# GOOD - Type-safe with autocompleteawait vault.request(HttpMethod.GET, url, grant_id=grant_id)# Also works - string methodsawait vault.request("GET", url, grant_id=grant_id)
Include Reasons for Audit
# GOOD - Reason shows up in audit logsresponse = await vault.request( HttpMethod.GET, url, grant_id=grant_id, reason="Fetching calendar events for weekly digest",)