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.
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 ...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 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, 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 |
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 |
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) |
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.