Skip to main content

Alter Vault Python SDK - OAuth token management with policy enforcement

Project description

Alter SDK for Python

Official Python SDK for Alter Vault - Credential management for agents with policy enforcement.

Features

  • Zero Token Exposure: Tokens are never exposed to developers - injected automatically
  • Single Entry Point: One method (vault.request()) for all provider APIs
  • Type-Safe Enums: Provider and HttpMethod enums with autocomplete
  • URL Templating: Path parameter substitution with automatic URL encoding
  • Automatic Audit Logging: All API calls logged with request metadata (HTTP method and URL) for full audit trail
  • Real-time Policy Enforcement: Every token request checked against current policies
  • Automatic Token Refresh: Tokens refreshed transparently by the backend
  • API Key and Custom Credential Support: Handles OAuth tokens, API keys, and custom credential formats automatically
  • Signed Requests: All SDK-to-backend requests are cryptographically signed for integrity, authenticity, and replay protection
  • Actor Tracking: First-class support for AI agent and backend service observability

Installation

pip install alter-sdk

Quick Start

import asyncio
from alter_sdk import AlterVault, ActorType, Provider, HttpMethod

async def main():
    vault = AlterVault(
        api_key="alter_key_...",
        actor_type=ActorType.AI_AGENT,
        actor_identifier="my-agent",
    )

    # Make API request -token injected automatically, never exposed
    response = await vault.request(
        "GRANT_ID",  # from Alter Connect (see below)
        HttpMethod.GET,
        "https://api.example.com/v1/resource",
        query_params={"maxResults": "10"},
    )
    events = response.json()
    print(events)

    await vault.close()

asyncio.run(main())

Where does grant_id come from?

OAuth grants (per-user, from end user action):

  1. Your end user completes OAuth via Alter Connect (frontend widget) or vault.connect() (headless)
  2. The onSuccess callback returns a grant_id (UUID) -one per user per account
  3. You save it in your database, mapped to your user
  4. You pass it to vault.request() when making API calls

Managed secrets (per-service, from developer action):

  1. You store credentials in the Developer Portal under Managed Secrets
  2. The portal returns a grant_id -one per stored credential, shared across your backend
  3. Use the same vault.request() -credentials are injected automatically
# You can also discover grant_ids programmatically:
result = await vault.list_grants(provider_id="google")
for grant in result.grants:
    print(f"{grant.grant_id}: {grant.account_display_name}")

Usage

The request() method returns the raw httpx.Response. The token is injected automatically and never exposed.

Simple GET Request

response = await vault.request(
    grant_id,
    HttpMethod.GET,
    "https://api.example.com/v1/resource",
)

POST with JSON Body

response = await vault.request(
    grant_id,
    HttpMethod.POST,
    "https://api.example.com/v1/items",
    json={"name": "New Item", "price": 99.99},
    reason="Creating new item",
)

URL Path Templating

response = await vault.request(
    grant_id,
    HttpMethod.PUT,
    "https://api.example.com/v1/items/{item_id}",
    path_params={"item_id": "123"},
    json={"price": 89.99},
)

Query Parameters and Extra Headers

response = await vault.request(
    grant_id,
    HttpMethod.POST,
    "https://api.notion.com/v1/databases/{db_id}/query",
    path_params={"db_id": "abc123"},
    extra_headers={"Notion-Version": "2022-06-28"},
    json={"page_size": 10},
)

Context Manager

async with AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) as vault:
    response = await vault.request(
        grant_id,
        HttpMethod.GET,
        "https://api.example.com/v1/resource",
    )
# Automatically closed

Note: After close() is called, subsequent request() calls raise AlterSDKError. close() is idempotent -calling it multiple times is safe.

Using Managed Secrets

For your own APIs with API keys or service tokens (no OAuth flow needed):

async with AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) as vault:
    response = await vault.request(
        "MANAGED_SECRET_GRANT_ID",  # from Developer Portal
        HttpMethod.GET,
        "https://api.internal.com/v1/data",
    )

The credential is injected automatically as the configured header type (Bearer, API Key, Basic Auth).

Grant Management

List Grants

Retrieve OAuth grants for your app, optionally filtered by provider:

from alter_sdk import AlterVault, ActorType

async with AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.BACKEND_SERVICE,
    actor_identifier="my-service",
) as vault:
    # List all grants
    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, offset=0)
    print(f"Total: {result.total}, Has more: {result.has_more}")
Parameter Type Default Description
provider_id str | None None Filter by provider (e.g., "google")
limit int 100 Max grants to return
offset int 0 Pagination offset

Returns GrantListResult with: grants (list[GrantInfo]), total, limit, offset, has_more.

Each GrantInfo has:

Field Type Description
grant_id str Unique grant identifier (UUID). Not .id — always use .grant_id
provider_id str Provider slug (e.g., "google", "slack")
scopes list[str] Granted OAuth scopes
account_identifier str | None Provider account email or username
account_display_name str | None Human-readable account name
status str Grant status (e.g., "active")
scope_mismatch bool True if granted scopes don't match requested scopes
expires_at str | None Expiry timestamp (if a grant policy TTL was set)
created_at str When the grant was created
last_used_at str | None When the grant was last used for an API call

Create Connect Session

Generate a session URL for end-users to authenticate with 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}")
print(f"Expires in: {session.expires_in}s")
Parameter Type Default Description
allowed_providers list[str] | None None Restrict to specific providers
return_url str | None None Redirect URL after OAuth flow

Returns ConnectSession with: session_token, connect_url, expires_in, expires_at.

Headless Connect (from code)

For CLI tools, Jupyter notebooks, and backend scripts -opens the browser, waits for the user to complete OAuth, and returns the result:

results = await vault.connect(
    providers=["google"],
    grant_policy={  # optional TTL bounds
        "max_ttl_seconds": 86400,
        "default_ttl_seconds": 3600,
    },
    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})")
    print(f"Account: {result.account_identifier}")

# Now use the grant_id with vault.request()
response = await vault.request(
    results[0].grant_id,
    HttpMethod.GET,
    "https://api.example.com/v1/resource",
)
Parameter Type Default Description
providers list[str] | None None Restrict to specific providers
timeout int 300 Max seconds to wait for completion
poll_interval float 2.0 Seconds between status checks
grant_policy dict | None None TTL bounds (max_ttl_seconds, default_ttl_seconds)
open_browser bool True Open browser automatically

Returns list[ConnectResult] -one per connected provider. Each has: grant_id, provider_id, account_identifier, scopes, and optionally grant_policy (if a TTL was set).

Raises ConnectTimeoutError if the user doesn't complete in time, ConnectDeniedError if the user denies authorization, ConnectConfigError if the OAuth app is misconfigured.

AI Agent Actor Tracking

from alter_sdk import AlterVault, ActorType, Provider, HttpMethod

vault = AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.AI_AGENT,
    actor_identifier="email-assistant-v2",
    actor_name="Email Assistant",
    actor_version="2.0.0",
    framework="langgraph",
)

response = await vault.request(
    grant_id,
    HttpMethod.GET,
    "https://api.example.com/v1/resource",
    run_id="550e8400-e29b-41d4-a716-446655440000",  # auto-generated UUID if omitted
    thread_id="thread-xyz",
    tool_call_id="call_abc_123",
    tool_id="read_calendar",                        # specific tool name
)

Note: run_id is auto-generated as a UUID v4 if not provided. All sub-actions within a single request() call share the same run_id for audit log grouping.

Multi-Agent Deployments

Each agent must create its own AlterVault instance with a unique actor identity. Do not share a single instance across agents.

# Each agent gets its own vault instance
email_agent = AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.AI_AGENT,
    actor_identifier="email-assistant-v2",
    actor_name="Email Assistant",
)

calendar_agent = AlterVault(
    api_key="alter_key_...",
    actor_type=ActorType.AI_AGENT,
    actor_identifier="calendar-agent-v1",
    actor_name="Calendar Agent",
)

# Audit logs and policies are tracked per agent
await email_agent.request(
    gmail_grant_id,  # from Alter Connect
    HttpMethod.GET,
    "https://api.example.com/v1/messages",
)
await calendar_agent.request(
    calendar_grant_id,  # from Alter Connect
    HttpMethod.GET,
    "https://api.example.com/v1/resource",
)

# Clean up each instance
await email_agent.close()
await calendar_agent.close()

Configuration

from alter_sdk import AlterVault, ActorType

vault = AlterVault(
    api_key="alter_key_...",              # Required: Alter Vault API key
    actor_type=ActorType.AI_AGENT,        # Required: ActorType enum
    actor_identifier="my-agent",          # Required: Unique identifier
    timeout=30.0,                         # Optional: HTTP timeout in seconds
    actor_name="My Agent",               # Optional: Human-readable name
    actor_version="1.0.0",               # Optional: Version string
    framework="langgraph",               # Optional: AI framework
    client_type="cursor",                # Optional: MCP client type
)

End-User Authentication

For identity-based grant resolution, authenticate end users via the configured IDP:

# Trigger browser-based IDP login
auth = await vault.authenticate(timeout=300.0)

print(auth.user_info)  # {"sub": "user-123", "email": "user@example.com", ...}

# Subsequent requests automatically use identity resolution
response = await vault.request(
    None,  # no grant_id — resolved via user identity
    HttpMethod.GET,
    "https://api.example.com/endpoint",
    provider="<provider>",
)

The authenticate() method opens the IDP login page in the user's browser, polls for completion, and returns an AuthResult with the user's token and profile info. After authentication, request() calls with provider= automatically resolve to the correct grant for that user.

Error Handling

The SDK provides a typed exception hierarchy so you can handle each failure mode precisely:

AlterSDKError (base)
├── BackendError                     # Generic backend error
│   ├── ReAuthRequiredError          # User must re-authorize via Alter Connect
│   │   ├── GrantExpiredError        # 403 — grant TTL elapsed
│   │   ├── CredentialRevokedError   # 400 — underlying auth permanently broken (revoked, invalid_grant)
│   │   ├── GrantRevokedError        # 400 — grant revoked
│   │   └── GrantDeletedError        # 410 — user disconnected via Wallet (new ID on re-auth)
│   ├── GrantNotFoundError           # 404 — wrong grant_id
│   └── PolicyViolationError         # 403 — policy denied (business hours, IP, etc.)
├── ConnectFlowError                 # Headless connect() failed
│   ├── ConnectDeniedError           # User clicked Deny
│   ├── ConnectConfigError           # OAuth app misconfigured
│   └── ConnectTimeoutError          # User didn't complete in time
├── ProviderAPIError                 # Provider returned 4xx/5xx
│   └── ScopeReauthRequiredError    # 403 + scope mismatch — user must re-authorize
└── NetworkError                     # Backend or provider unreachable
    └── TimeoutError                 # Request timed out (safe to retry)
from alter_sdk import AlterVault, Provider, HttpMethod
from alter_sdk.exceptions import (
    AlterSDKError,              # Base exception (including validation errors)
    BackendError,               # Generic backend error
    ReAuthRequiredError,        # Parent for all re-auth errors
    GrantDeletedError,          # User disconnected via Wallet — new ID on re-auth (410)
    GrantExpiredError,          # TTL expired — user must re-authorize (403)
    GrantNotFoundError,         # Wrong grant_id — check for typos (404)
    CredentialRevokedError,     # Underlying auth permanently broken — revoked/invalid_grant (400)
    GrantRevokedError,          # Grant revoked (400)
    PolicyViolationError,       # Policy denied access — business hours, IP, etc. (403)
    ConnectFlowError,           # Headless connect() failed (denied, provider error)
    ConnectDeniedError,         # User denied authorization
    ConnectConfigError,         # OAuth app misconfigured
    ConnectTimeoutError,        # Headless connect() timed out
    NetworkError,               # Backend or provider unreachable
    TimeoutError,               # Request timed out (subclass of NetworkError)
    ProviderAPIError,           # Provider API returned error (4xx/5xx)
    ScopeReauthRequiredError,   # 403 + scope mismatch (subclass of ProviderAPIError)
)

try:
    response = await vault.request(
        grant_id,
        HttpMethod.GET,
        "https://api.example.com/v1/resource",
    )

# --- Grant unusable — user must re-authorize via Alter Connect ---
except GrantExpiredError:
    # TTL set during Connect flow has elapsed
    print("Grant expired — prompt user to re-authorize")
except CredentialRevokedError:
    # Underlying auth permanently broken — user revoked at provider, refresh token
    # expired, or token refresh permanently failed (invalid_grant)
    print("Connection revoked — prompt user to re-authorize")
except GrantRevokedError:
    # Grant itself was revoked
    print("Grant revoked — prompt user to re-authorize")
except GrantDeletedError:
    # User disconnected via Wallet — re-auth generates a NEW grant_id
    print("Grant deleted — prompt user to re-connect, store the new ID")
except GrantNotFoundError:
    # No grant with this ID exists — check for typos or stale references
    print("Grant not found — verify your grant_id")

# --- Policy restrictions — may resolve on its own ---
except PolicyViolationError as e:
    # Business hours, IP allowlist, or other policy denial
    print(f"Policy denied: {e.message} (reason: {e.policy_error})")
    print("Check policy config in Developer Portal, or queue work for later")

# --- Transient / infrastructure errors — safe to retry ---
except NetworkError as e:
    # TimeoutError is a subclass, so this catches both
    print(f"Network issue — retry with backoff: {e.message}")

except ScopeReauthRequiredError as e:
    print(f"Scope mismatch on {e.grant_id} - user needs to re-authorize")
    # Create a new Connect session so the user can grant updated scopes

# --- Provider errors ---
except ProviderAPIError as e:
    print(f"Provider error {e.status_code}: {e.response_body}")

Re-authorization and Grant IDs

When a user re-authorizes through Alter Connect, the same grant_id is preserved in most cases. The existing grant record is updated in place with fresh tokens. You do not need to update your stored grant_id.

The exception is GrantDeletedError — the user disconnected via the Wallet, so re-authorization creates a new grant with a new grant_id. Store the new ID from the ConnectResult.

Exception Same grant_id after re-auth?
GrantExpiredError Yes
CredentialRevokedError Yes
GrantRevokedError Yes
GrantDeletedError No — new ID generated
GrantNotFoundError N/A — ID never existed

Supported Providers

The SDK includes type-safe Provider enums for all 66 supported providers. Use them for filtering grants or as documentation -- request() takes a grant_id string, not a provider enum.

from alter_sdk import Provider

# Custom providers (full OAuth implementations)
Provider.GOOGLE       # "google"
Provider.GITHUB       # "github"
Provider.SLACK        # "slack"
Provider.CALENDLY     # "calendly"
Provider.CLICKUP      # "clickup"
Provider.CANVA        # "canva"
# ... and 23 more (see Provider enum for full list)

# Config-driven providers (45 total) -- examples:
Provider.HUBSPOT      # "hubspot"
Provider.SALESFORCE   # "salesforce"
Provider.STRIPE       # "stripe"
Provider.MICROSOFT    # "microsoft"
Provider.DISCORD      # "discord"
Provider.SPOTIFY      # "spotify"
Provider.LINKEDIN     # "linkedin"
Provider.DROPBOX      # "dropbox"
Provider.FIGMA        # "figma"
# ... and 36 more (see Provider enum for full list)

# Usage: filter grants by provider
result = await vault.list_grants(provider_id=Provider.HUBSPOT)

# Usage: make requests with grant_id
await vault.request(grant_id, HttpMethod.GET, url)
All 66 providers

Acuity Scheduling, Adobe, Aircall, Airtable, Apollo, Asana, Atlassian, Attio, Autodesk, Basecamp, Bitbucket, Bitly, Box, Brex, Cal.com, Calendly, Canva, ClickUp, Close, Constant Contact, Contentful, Deel, Dialpad, DigitalOcean, Discord, DocuSign, Dropbox, eBay, Eventbrite, Facebook, Figma, GitHub, Google, HubSpot, Instagram, Linear, LinkedIn, Mailchimp, Mercury, Microsoft, Miro, Monday, Notion, Outreach, PagerDuty, PayPal, Pinterest, Pipedrive, QuickBooks, Ramp, Reddit, RingCentral, Salesforce, Sentry, Slack, Snapchat, Spotify, Square, Squarespace, Stripe, TikTok, Todoist, Twitter, Typeform, Webex, Webflow

Requirements

  • Python 3.11+
  • httpx[http2]
  • pydantic

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alter_sdk-0.7.0.tar.gz (45.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alter_sdk-0.7.0-py3-none-any.whl (42.8 kB view details)

Uploaded Python 3

File details

Details for the file alter_sdk-0.7.0.tar.gz.

File metadata

  • Download URL: alter_sdk-0.7.0.tar.gz
  • Upload date:
  • Size: 45.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for alter_sdk-0.7.0.tar.gz
Algorithm Hash digest
SHA256 f4ae87d8ee941aeb4076f12aa7313e876dab04582b88e10c08980bedd1d68a1a
MD5 ab5fb330224b352d3599286b10c9783b
BLAKE2b-256 1b277b16cf24162b6796d6abc5ec5943e349b0554342c3c8d9b033bfea981b96

See more details on using hashes here.

Provenance

The following attestation bundles were made for alter_sdk-0.7.0.tar.gz:

Publisher: python-sdk-release.yml on AlterAIDev/Alter-Vault

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file alter_sdk-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: alter_sdk-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 42.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for alter_sdk-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 db36fabae273ac1b0d699096a85f2d7109786406d1546aa4fd98e45a872135e9
MD5 568335bfcd3010cfdbb5d8b5f063ae03
BLAKE2b-256 8f6df35eaf710197e0f89556c63b96c4f642ec898ee8d57dca09da1b4bdac3bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for alter_sdk-0.7.0-py3-none-any.whl:

Publisher: python-sdk-release.yml on AlterAIDev/Alter-Vault

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page