Reference
Errors
Typed exception hierarchy for the Python SDK.
Every SDK exception inherits from AlterSDKError. Branching on the typed subclasses is the supported way to handle failure modes — never parse error message strings.
from alter_sdk import ( AlterSDKError, AlterValueError, BackendError, ReAuthRequiredError, GrantExpiredError, GrantRevokedError, GrantDeletedError, GrantNotFoundError, AgentDelegationMissingError, GrantNotDelegableError, CredentialRevokedError, AmbiguousGrantError, GrantCandidate, NoDelegatedGrantError, PolicyViolationError, StepUpRequiredError, RedactDischargeFailedError, InsufficientScopeError, TokenRefreshInProgressError, QuotaExceededError, RateLimitError, ConnectFlowError, ConnectDeniedError, ConnectConfigError, ConnectTimeoutError, ProviderAPIError, ScopeReauthRequiredError, ProviderUnauthorizedError, NetworkError, TimeoutError, ApprovalError, ApprovalDeniedError, ApprovalExpiredError, ApprovalTimeoutError, ApprovalExecutionFailedError, AgentError, AgentNotFoundError, AgentNameExistsError, CallerAgentMismatchError, CallerAgentResolutionError, AgentConcurrentUpdateError, AgentInactiveError, AgentRevokedError, InvalidKeyError, KeyRevokedError, KeyExpiredError, KeyRotationExpiredError, KeyInactiveError, KeyAppNotFoundError, KeyAlreadyRevokedError, KeyNotFoundError, LastActiveKeyError, AgentKeyLimitError, MeRequiresAgentKeyError, AgentCannotMintSubagentsError, RestrictedGrantRequiresProxyError, SiblingLabelConflictError, IdempotencyKeyBodyMismatchError, IdempotencyKeyAgentRevokedError, IdempotencyKeyAgentInactiveError,)The full hierarchy and a per-error recovery playbook are documented at /reference/errors. This page is a short index of what’s exported from alter_sdk so you know what to import.
Catching by category
Section titled “Catching by category”from alter_sdk import ReAuthRequiredError, BackendError
try: resp = await app.request("GET", url, provider="provider-id")except ReAuthRequiredError: # GrantExpiredError, GrantRevokedError, CredentialRevokedError, GrantDeletedError # — user must re-authorize via Connect ...except BackendError: # Any backend-origin failure not in the re-auth bucket ...Common attributes
Section titled “Common attributes”Every typed error carries .message and .details: dict[str, Any]. Specific subclasses add fields:
| Exception | Extra attributes |
|---|---|
GrantRevokedError, TokenRefreshInProgressError | grant_id |
CredentialRevokedError | grant_id, provider_id, app_user_id |
GrantExpiredError | provider_id, agent_id, app_user_id — populated only on the delegation-path expiry (a TTL-lapsed delegation chain); None on the root-path TTL expiry |
GrantNotFoundError | provider_id, agent_id, app_user_id (no grant_id — the grant lookup is what failed) |
AgentDelegationMissingError | subclass of GrantNotFoundError (same provider_id / agent_id / app_user_id fields) — raised on the agent path when a grant_id isn’t reachable by this agent; delegate via Connect or resolve by provider |
GrantNotDelegableError | (subclass of BackendError) — raised by agent.delegate() when the held grant is not delegable, or a managed secret’s operator ceiling blocks re-delegable grants (HTTP 403 grant_not_delegable) |
ScopeReauthRequiredError | grant_id, provider_id |
AmbiguousGrantError | provider_id, account_identifiers, account_was_provided, app_user_ids, grant_ids, candidates |
NoDelegatedGrantError | provider_id, agent_id, app_user_id |
PolicyViolationError | policy_error |
StepUpRequiredError | subclass of PolicyViolationError; policy_error="step_up_required" and max_session_age_seconds carries the session-freshness ceiling when supplied |
RedactDischargeFailedError | subclass of PolicyViolationError; policy_error="redact_discharge_failed" and the request was denied because required redaction could not be applied |
SiblingLabelConflictError | label — the sibling label already active on the credential |
QuotaExceededError | retry_after — seconds until the quota window resets (prefers the Retry-After response header over the body value, so it can differ from details["retry_after"], and is capped at 2,678,400 seconds / 31 days — a quota window is a calendar period up to a month long, and the response reports the longest exhausted one); may be None. Raised on a request-quota policy denial; the SDK never auto-retries it |
RateLimitError | retry_after — seconds to wait before retrying (prefers the Retry-After response header over the body value, so it can differ from details["retry_after"], and is capped at 3,600 seconds — one hour, above the platform’s legitimate retry guidance); scope ("ip", "api_key" or "organization") — which platform ceiling was hit (a plain str, not a closed enum, so a ceiling added later stays parseable; the known values are exported as RATE_LIMIT_SCOPE_VALUES, ordered outermost-first); limit; window_seconds. scope, limit and window_seconds may each be None — not every response carrying this error code supplies them, so branch on a missing value rather than assuming one. A transient throttle, not a denial: the same request succeeds once the reported wait elapses |
InsufficientScopeError | required, granted, missing, scope_version, current_scope_version, scope_version_mismatch, documentation_url (aliases: required_scopes, granted_scopes, docs_url) |
ProviderAPIError, ScopeReauthRequiredError, ProviderUnauthorizedError | status_code, response_body |
ScopeReauthRequiredError | also missing_scopes (from WWW-Authenticate: Bearer error="insufficient_scope") |
ProviderUnauthorizedError | grant_id, provider_id — provider returned 401 (credential rejected provider-side); re-authorize an OAuth grant via a new Connect session, or update the stored managed secret. A 401 whose WWW-Authenticate challenge carries error="insufficient_scope" (RFC 6750 — the credential is still valid, it just lacks a scope) surfaces as a generic ProviderAPIError instead |
ConnectTimeoutError | details["reason"] is "poll_deadline_elapsed" when the local polling deadline wins, or "session_expired" when a session observed pending expires before Alter receives a callback. With no callback, an exact provider-side error is unknowable; the provider may have retained an error page, the browser may have closed, or the user may have abandoned the flow |
ApprovalError (and subclasses) | approval_id |
AgentError (and subclasses) | code, hint |
GrantCandidate is a supporting dataclass, not an exception. Each item in AmbiguousGrantError.candidates has grant_id, label, account_identifier, and account_display_name; retry with the chosen grant_id, or with provider + label when a stable sibling address is preferable.
Recovery patterns
Section titled “Recovery patterns”Identity-mode ambiguity — retry with account=:
from alter_sdk import AmbiguousGrantError
try: resp = await app.request("GET", url, provider="provider-id")except AmbiguousGrantError as e: resp = await app.request( "GET", url, provider="provider-id", account=e.account_identifiers[0], )Missing or broken credential — mint a recovery Connect session:
from alter_sdk import NoDelegatedGrantError, CredentialRevokedError
try: resp = await agent.request("GET", url, provider="provider-id")except (NoDelegatedGrantError, CredentialRevokedError) as e: session = await agent.create_connect_session_for_error(e) redirect_to(session.connect_url) results = await agent.poll_connect_session(session.session_token)Transient refresh conflict — exponential backoff:
import asynciofrom alter_sdk import TokenRefreshInProgressError
for attempt in range(3): try: resp = await app.request(...) break except TokenRefreshInProgressError: await asyncio.sleep(1 * (2 ** attempt))Quota window exhausted — wait for the window to reset:
import asynciofrom alter_sdk import QuotaExceededError
try: resp = await app.request(...)except QuotaExceededError as e: # A request-quota policy denial — the SDK never auto-retries it. await asyncio.sleep(e.retry_after if e.retry_after is not None else 60) resp = await app.request(...)Insufficient scope at the version mismatch — rotate the key:
from alter_sdk import InsufficientScopeError
try: await app.agents.create( name="bot", scopes={"provider-id": ["resource:read"]}, )except InsufficientScopeError as e: if e.scope_version_mismatch: print(f"Key minted at v{e.scope_version}; server at v{e.current_scope_version}. Rotate.") else: print(f"Missing: {e.missing}. See {e.docs_url}")See /reference/errors for the full hierarchy with descriptions and recovery guidance for every error.