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 requirealter-sdk>=0.28.0. Version 0.27.0 and earlier do 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, CredentialIdRequiredError, EndUserCredentialNotAssignableError, 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 provider’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 |
RestrictedGrantRequiresProxyError | subclass of BackendError; no extra attributes. The grant is proxy-only, so request() (raw credential release) refuses it; use proxy_request(). details["error"] names why: restricted_grant_requires_proxy (the grant carries method/endpoint restrictions), delegated_grant_requires_proxy (a delegated or scope-narrowed grant), hitl_grant_requires_proxy (the grant requires approval) or user_limits_require_proxy (the grant’s user set a usage limit on it — a quota, a time window or an operation rule — on an OAuth or a managed-secret grant alike). Each of these is enforceable only when the backend makes the provider call itself. A fix-your-call error: switch the call site, do not retry |
SiblingLabelConflictError | label — the sibling label already active on the credential |
CredentialIdRequiredError | credentials — a list of CredentialChoice (credential_id, label): every developer credential the provider holds. Raised by create_managed_secret_grant() when the provider holds several and credential_id was omitted (HTTP 409 credential_id_required); retry with one of them |
EndUserCredentialNotAssignableError | (subclass of BackendError) — credential_id named an end user’s own key, which has one grant (the user’s) and is never assigned (HTTP 409 end_user_credential_not_assignable) |
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. For a managed-secret grant, the application rotates a developer credential (Developer Portal, or alter managed-secrets credentials rotate); an end user replaces their own key through a managed-secret Connect session with repair_grant_id. 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, so the SDK raises instead of returning a session that would not do what was asked. On create_managed_secret_connect_session() (App and Agent), details["feature"] names what is missing: managed_secret_connect_provider — the backend still requires template_slug when only provider was sent, refuses a parameter it does not know (provider, agent, requested_grant, grant_policy, direct, repair_grant_id), answered a session without the path a current backend always reports (it ignored those parameters), or still requires an agent on a session that names none for an app key; agent_managed_secret_connect_session — a session naming no agent (the Agent method on an agent’s own key, or the application’s own use of the user’s key) against a backend that still requires delegated_agent_id; managed_secret_connect_delegable — delegable=True when the backend did not echo delegable_requested, or echoed it as False. details["status_code"] carries the HTTP status. Retrying against the same backend cannot help: upgrade the backend, or until then request the session with an app key and an agent — app.get_agent(agent_id).create_managed_secret_connect_session(template_slug=...) or the App method with template_slug and delegated_agent_id — without delegable |
ConnectConfigError | details["error"] names the cause (for example managed_oauth_scope_approval_required). create_managed_secret_connect_session() also raises it when the session cannot be created until an administrator or the application changes something (retrying does not help): secret_not_found (HTTP 404 — no active provider of the application has the provider slug); secret_template_mismatch (HTTP 422 — provider was not created from template_slug); idp_signin_not_configured (HTTP 409, details["retryable"] is False — the application’s identity provider cannot sign users in to approve; 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); binding_hosts_required (HTTP 409 — the user would enter a key, but the provider derives its hosts from the key and has no host patterns set: an administrator sets them); provider_contract_repair_required (HTTP 409 — the provider’s stored contract is out of date: an administrator rotates its existing developer credential once before a key can be entered); no_eligible_grants (HTTP 409 — no path can succeed: on a key entry with an agent, that agent could never be delegated the key, details["details"]["reason"] agent_not_allowlisted or delegation_depth_exceeded; or a template_slug-only request whose template names no single provider found no grant the user holds, reason no_grant, grant_expired, delegation_depth_exceeded, group_source_not_allowed or agent_not_allowlisted — see the reasons). binding_hosts_required, provider_contract_repair_required and no_eligible_grants reach only a key bound to no managed agent: 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 receives them; the hosted page tells the user instead |
ApprovalError (and subclasses) | approval_id |
AgentError (and subclasses) | code, hint |
CredentialChoice is a supporting dataclass, not an exception: each item in CredentialIdRequiredError.credentials has credential_id (what create_managed_secret_grant(credential_id=...) takes) and label.
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, requirealter-sdk>=0.28.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.