Skip to content

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, and KeyAppNotFoundError require alter-sdk>=0.23.1. Version 0.22.0 does not export them; upgrade before importing or catching these classes. MemoryIsolationError, its code attribute and the MEMORY_ISOLATION_CODES vocabulary 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.

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

Every typed error carries .message and .details: dict[str, Any]. Specific subclasses add fields:

ExceptionExtra attributes
GrantRevokedError, TokenRefreshInProgressErrorgrant_id
CredentialRevokedErrorgrant_id, provider_id, app_user_id
GrantExpiredErrorprovider_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
GrantNotFoundErrorprovider_id, agent_id, app_user_id (no grant_id — the grant lookup is what failed)
AgentDelegationMissingErrorsubclass 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)
ScopeReauthRequiredErrorgrant_id, provider_id
AmbiguousGrantErrorprovider_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
NoDelegatedGrantErrorprovider_id, agent_id, app_user_id
PolicyViolationErrorpolicy_error
StepUpRequiredErrorsubclass of PolicyViolationError; policy_error="step_up_required" and max_session_age_seconds carries the session-freshness ceiling when supplied
RedactDischargeFailedErrorsubclass of PolicyViolationError; policy_error="redact_discharge_failed" and the request was denied because required redaction could not be applied
SiblingLabelConflictErrorlabel — the sibling label already active on the credential
QuotaExceededErrorretry_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
RateLimitErrorretry_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}
InsufficientScopeErrorrequired, granted, missing, scope_version, current_scope_version, scope_version_mismatch, documentation_url (aliases: required_scopes, granted_scopes, docs_url)
ProviderAPIError, ScopeReauthRequiredError, ProviderUnauthorizedErrorstatus_code, response_body
ScopeReauthRequiredErroralso missing_scopes (from WWW-Authenticate: Bearer error="insufficient_scope")
ProviderUnauthorizedErrorgrant_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
ConnectTimeoutErrordetails["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
ServerFeatureUnsupportedErrorsubclass 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
ConnectConfigErrordetails["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.

Identity-mode ambiguity — choose deliberately, never the first candidate:

from alter_sdk import AmbiguousGrantError
try:
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 asyncio
from 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 asyncio
from 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.

Version compatibility: BlockedDestinationError, and RateLimitError for a non-JSON 429, require an alter-sdk release after 0.27.0. Version 0.27.0 and earlier raise a generic BackendError for 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.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.