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.

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.

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 is not delegable, or a managed secret’s operator ceiling blocks re-delegable grants (HTTP 403 grant_not_delegable)
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
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
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)
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.

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.