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.
Version compatibility: this page tracks the latest published SDK line.
CallerAgentMismatchError,CallerAgentResolutionError,KeyExpiredError,KeyRotationExpiredError,KeyInactiveError, andKeyAppNotFoundErrorrequirealter-sdk>=0.23.1. Version 0.22.0 does not export them; upgrade before importing or catching these classes.MemoryIsolationError, itscodeattribute and theMEMORY_ISOLATION_CODESvocabulary are unreleased: they ship in the next release after 0.27.0, which does not export them.
from alter_sdk import ( AlterSDKError, AlterValueError, MemoryIsolationError, MemoryIsolationCode, MEMORY_ISOLATION_CODES, BackendError, BlockedDestinationError, ServerFeatureUnsupportedError, 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 ...Within the re-auth bucket, the subclasses say why: GrantExpiredError — the
grant’s TTL elapsed; GrantRevokedError — the grant was explicitly revoked
(Wallet, disconnect, or an administrator), raised on every subsequent
App-side call addressing it (Agent callers and calls addressing a
delegated grant instead receive the reason-neutral GrantDeletedError, so a
delegated agent cannot probe whether its access was deliberately revoked);
CredentialRevokedError — the underlying OAuth connection is permanently
broken; GrantDeletedError — the grant is permanently gone with no more
specific signal (re-authorization issues a new grant id).
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 has no consenting user or is not delegable, the credential’s operator ceiling is off (a managed secret’s delegation policy or the OAuth provider’s agent sharing setting), or delegable=True asks for a child that the depth ceiling leaves no further hop (HTTP 403 grant_not_delegable). detail_reason names which: no_consenting_user, holder_not_delegable, ceiling_off, no_onward_hop or not_requested (None when the backend sent none) |
ScopeReauthRequiredError | grant_id, provider_id |
AmbiguousGrantError | provider_id, account_identifiers, account_was_provided, app_user_ids, grant_ids, candidates, candidates_truncated — when candidates_truncated is True every disambiguator here is a bounded sample, so an absence from any of them proves nothing |
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. Also raised for a non-JSON 429 from an intermediary such as an edge provider: the limiting layer is unknown, so scope, limit and window_seconds are None, retry_after is set only from a Retry-After header, and details is {"status_code": 429} |
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. A user who signed in to the provider but closed Connect without confirming also ends here; when the Alter deployment holds new connections until Confirm, no connection from that session is usable |
ServerFeatureUnsupportedError | subclass of BackendError; no extra attributes. The Alter backend predates a feature the call used: Agent.create_managed_secret_connect_session against a backend that still requires delegated_agent_id (its request-validation 422 names that field as missing), or delegable=True on create_managed_secret_connect_session when the backend did not echo delegable_requested, or echoed it as False. The SDK raises instead of returning a session that would not do what was asked. details["feature"] names what is missing: agent_managed_secret_connect_session or managed_secret_connect_delegable. Retrying against the same backend cannot help: until it supports the feature, request the session with an app key — app.get_agent(agent_id).create_managed_secret_connect_session(...) or the App method — without delegable |
ConnectConfigError | details["error"] names the cause (for example managed_oauth_scope_approval_required). create_managed_secret_connect_session raises it with details["error"] == "idp_signin_not_configured" (HTTP 409, details["retryable"] is False) when the application’s identity provider cannot sign users in to approve the delegation; details["details"]["reason"] says what to change: missing_client_id or missing_client_secret — add the sign-in client ID and secret to the identity provider; issuer_cannot_sign_in — use an issuer that supports OpenID Connect sign-in (for WorkOS, the AuthKit issuer https://<slug>.authkit.app). It also raises it with details["error"] == "no_eligible_grants" (HTTP 409) when the user holds no credential the delegation could use; details["details"]["reason"] is no_grant, grant_expired, delegation_depth_exceeded, group_source_not_allowed or agent_not_allowlisted (see the reasons). Only a key bound to no managed agent receives no_eligible_grants: an app key, including an Agent from App.get_agent(), or an agent-typed alter_ak_… key minted under API Keys. A managed agent’s own key never does |
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 — choose deliberately, never the first candidate:
from alter_sdk import AmbiguousGrantErrortry: resp = await app.request("GET", url, provider="provider-id", user_token=user_jwt)except AmbiguousGrantError as e: # e.candidates: grant_id, label, account_identifier, account_display_name. # Let the user pick (or apply the account already saved on their profile), # persist the grant_id, and call by it with the user's JWT. grant_id = choose_grant(e.candidates) resp = await app.request("GET", url, grant_id=grant_id, user_token=user_jwt)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.
Destination refusal
Section titled “Destination refusal”Version compatibility:
BlockedDestinationError, andRateLimitErrorfor a non-JSON 429, require analter-sdkrelease after 0.27.0. Version 0.27.0 and earlier raise a genericBackendErrorfor both responses.
BlockedDestinationError inherits from BackendError. The requested destination
failed the provider’s host/path allowlist or the network security check. When
a call held for approval is refused as the approved call executes, the approval
wait raises ApprovalExecutionFailedError instead. Correct the URL before
retrying. Its details includes error: "blocked_destination", status_code,
and request_id when the server supplied one; use the request ID
when investigating the refusal. Private resolver addresses and credentials are
not part of the diagnostic contract.