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 require alter-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.

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 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)
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
RestrictedGrantRequiresProxyErrorsubclass 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
SiblingLabelConflictErrorlabel — the sibling label already active on the credential
CredentialIdRequiredErrorcredentials — 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)
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. 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
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, 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
ConnectConfigErrordetails["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.

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 alter-sdk>=0.28.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.