Reference
Errors
Every exception class, what it means, what to do.
The SDK error hierarchy is organized by developer action — the action to take is the first thing to know when an exception is caught. Both SDKs expose the same class names; the snippets below are Python but the TS hierarchy is identical (camelCase fields).
AlterSDKError├── AlterValueError — SDK rejected caller input; fix the code├── BackendError — Alter backend returned an error│ ├── ReAuthRequiredError — user must re-authorize via Connect│ │ ├── GrantExpiredError│ │ ├── GrantRevokedError│ │ ├── CredentialRevokedError│ │ └── GrantDeletedError│ ├── GrantNotFoundError — wrong grant_id; fix the code│ ├── AmbiguousGrantError — multiple grants matched; pick one│ ├── PolicyViolationError — policy denied; may resolve later│ ├── InsufficientScopeError — key scopes don't cover the route│ ├── NoDelegatedGrantError — agent has no access path to the provider│ ├── RestrictedGrantRequiresProxyError — restricted grant; use the proxy call path│ ├── SiblingLabelConflictError — label already in use on this connection│ ├── TokenRefreshInProgressError — transient 409; retry│ └── AgentError — managed-agent management│ ├── InvalidKeyError│ ├── AgentNotFoundError│ ├── AgentNameExistsError│ ├── MeRequiresAgentKeyError│ ├── KeyRevokedError│ ├── AgentInactiveError│ ├── AgentRevokedError│ ├── KeyNotFoundError│ ├── KeyAlreadyRevokedError│ ├── LastActiveKeyError│ ├── AgentCannotMintSubagentsError│ ├── AgentScopeNarrowingNotSupportedError│ ├── IdempotencyKeyBodyMismatchError│ ├── IdempotencyKeyAgentRevokedError│ └── IdempotencyKeyAgentInactiveError├── ConnectFlowError — Connect flow failed│ ├── ConnectDeniedError — user clicked Deny│ ├── ConnectConfigError — OAuth app misconfigured│ └── ConnectTimeoutError — user didn't complete in time├── ProviderAPIError — provider returned 4xx/5xx│ ├── ScopeReauthRequiredError — 403 + scope mismatch; re-authorize│ └── ProviderUnauthorizedError — 401; token rejected provider-side; re-authorize├── ApprovalError — HITL approval branch│ ├── ApprovalDeniedError│ ├── ApprovalExpiredError│ ├── ApprovalTimeoutError│ └── ApprovalExecutionFailedError└── NetworkError └── TimeoutError — request timed outCatching by action
Section titled “Catching by action”The hierarchy is built so that catching a parent class handles every leaf that requires the same response.
- “The user needs to re-authorize.” Catch
ReAuthRequiredError. Triggers: revoked grant, expired grant, broken credential, deleted grant. Response: show the Connect widget. - “The user needs to re-authorize with a wider scope.” Catch
ScopeReauthRequiredError(subclass ofProviderAPIError). Trigger: the provider returned 403 and the grant’s stored scopes don’t cover the route. Response: show the Connect widget; the new authorization upgrades the grant in place. - “The provider rejected the credential.” Catch
ProviderUnauthorizedError(subclass ofProviderAPIError). Trigger: the provider returned 401 on arequest()call — the credential was revoked, expired, or otherwise invalidated provider-side. One exception: a 401 whoseWWW-Authenticatechallenge carrieserror="insufficient_scope"(RFC 6750) means the credential is still valid but lacks a scope, so it surfaces as a genericProviderAPIErrorinstead. Response: for an OAuth grant, show the Connect widget (a new Connect session re-authorizes the grant); for a managed-secret grant, update the stored secret with a valid value. - “The agent has no access to this provider.” Catch
NoDelegatedGrantError. Trigger: an agent calledrequest(provider=...)and no delegation or agent-owned managed-secret grant resolved. Response: prompt a user to delegate, or issue a managed-secret grant to the agent. - “A transient backend condition.” Catch
TokenRefreshInProgressError. Trigger: a parallel refresh holds the lock. Response: retry with backoff. - “A network problem.” Catch
NetworkError. Trigger: connection failure, DNS, timeout. Response: retry with backoff (catchTimeoutErrorseparately if a different retry profile is needed for timeouts vs connection refused).
Selected exception details
Section titled “Selected exception details”AmbiguousGrantError
Section titled “AmbiguousGrantError”Raised when provider-based resolution matches more than one grant. Alter never silently picks a grant — there is no “default grant” or “most recently connected wins” behavior. The error enumerates the candidates so the caller (or the end user, conversationally) chooses explicitly. The candidates are always scoped to the caller’s own accessible grants — the error can never reveal a grant the caller could not use.
| Attribute | Description |
|---|---|
provider_id | The provider whose grants matched. |
candidates | One entry per matching grant: grant_id and label (the sibling address — retry with provider + label), plus, where the caller may see them, account_identifier and account_display_name. Retry with the chosen grant_id — the universal disambiguator. |
account_identifiers | List of matching accounts; show to the user to pick. |
account_was_provided | True if account= was already passed and still produced an ambiguity (sign of a mis-set account). |
app_user_ids | (Agent flavor) UUIDs of the users with matching delegations. For an agent holding delegations from multiple users, candidates carry grant IDs and labels only — other users’ account identifiers are never exposed to the agent. |
grant_ids | (Managed-secret flavor) IDs of the matching grants when one user delegated multiple managed-secret grants of the same template to the agent. Retry with the chosen grant_id. |
Pass the chosen grant_id (or account=<chosen> / label=<chosen> /
user_token=<jwt> for the account / label / agent flavors) to disambiguate
on retry. For production workloads, prefer addressing grants by grant_id
from the start — the Connect flow returns it at consent time, and it stays
unambiguous no matter how many grants a user adds later.
RestrictedGrantRequiresProxyError
Section titled “RestrictedGrantRequiresProxyError”Raised when raw-token retrieval is attempted against a grant whose policy
carries method/endpoint restrictions. Restricted grants are proxy-only by
design — a token handed to the caller could not be held to the restriction —
and there is no opt-out. Switch the call to the SDK’s proxied execution path
(proxy_request() / proxyRequest()); restriction denials there surface as
PolicyViolationError.
SiblingLabelConflictError
Section titled “SiblingLabelConflictError”Raised by mint_grant() / mintGrant() when the requested label is already
held by an active grant on the same connection. Carries label. Pick a
different label, or revoke the holder first — revoking a grant frees its
label.
InsufficientScopeError
Section titled “InsufficientScopeError”Raised when the calling key — possibly after per-call attenuation — lacks a scope the backend route requires.
| Attribute | Description |
|---|---|
required | Scopes the route requires. |
granted | Scopes the key has, post-intersection with constraints. |
missing | required \ granted. |
scope_version | Catalog version the key was minted against. |
current_scope_version | Catalog version the server is on. |
scope_version_mismatch | True iff the missing scope only exists at the server’s version — rotate the key. |
documentation_url | Per-scope docs link. |
ScopeReauthRequiredError
Section titled “ScopeReauthRequiredError”Raised when the provider returns 403 and the grant’s stored scopes don’t cover the route.
| Attribute | Description |
|---|---|
grant_id | The grant that needs re-authorization. |
provider_id | The provider. |
missing_scopes | When parsed from a WWW-Authenticate: insufficient_scope challenge, the specific scopes missing. None when the source was a pre-flight backend flag. |
status_code, response_body | The provider’s raw response. |
Pattern:
try: await app.request(...)except ScopeReauthRequiredError as e: session = await app.create_connect_session(allowed_providers=[e.provider_id]) notify_user(session.connect_url)ProviderUnauthorizedError
Section titled “ProviderUnauthorizedError”Raised when the provider returns 401 to a request() call — the provider rejected the credential (revoked, expired, or otherwise invalidated provider-side). The 401 sibling of ScopeReauthRequiredError; both extend ProviderAPIError, so existing ProviderAPIError handlers keep catching it. A 401 whose WWW-Authenticate challenge carries error="insufficient_scope" (RFC 6750) is deliberately excluded — the credential is still valid, it just lacks a scope — and surfaces as a generic ProviderAPIError instead.
| Attribute | Description |
|---|---|
grant_id | The grant whose credential was rejected. |
provider_id | The provider. |
status_code, response_body | The provider’s raw response. |
Recovery depends on the grant family:
- OAuth grant — re-authorize via a new Connect session (same pattern as
ScopeReauthRequiredError:create_connect_session(allowed_providers=[e.provider_id])and surface the URL to the user). - Managed-secret grant — the stored secret is no longer accepted by the provider; update it with a valid value.
PolicyViolationError
Section titled “PolicyViolationError”Raised when a policy denies the call.
| Attribute | Description |
|---|---|
policy_error | Backend-supplied identifier for the violated policy (e.g., outside_business_hours, ip_not_allowed, rate_limit_exceeded). |
May resolve on its own (time-of-day window opens, rate limit decays).
LastActiveKeyError
Section titled “LastActiveKeyError”Raised by agents.revoke_key() when revoking would leave the agent with zero non-revoked keys.
Recovery: mint a replacement first, deploy it, then retry the revoke. Or pass force=True to deliberately brick the agent.
AgentError family
Section titled “AgentError family”All inherit from BackendError. Each carries a stable code attribute (agent_not_found, agent_inactive, key_revoked, …). Switch on err.code (not on the message) when handling multiple agent error types.
Agent codes without typed SDK wrappers
Section titled “Agent codes without typed SDK wrappers”A few backend agent codes surface as a plain BackendError rather than a dedicated subclass. They are still stable contracts — branch on err.details['error'] (Python) / err.details.error (TypeScript) when handling them.
details['error'] | HTTP | Meaning |
|---|---|---|
agent_cannot_read_peer_agents | 403 | An agent key tried to read another agent’s record. The canonical self-introspection path is agent.me(). |
agent_scope_not_allowed | 403 | The agent key called a route whose target provider / OAuth scope / managed-secret name is not in the agent’s scope allowlist. Widen the allowlist on the agent. |
idempotency_record_corrupted | 500 | An idempotency replay pointed at an agent or key that no longer exists. Retry with a fresh idempotency key; the underlying corruption is operator-investigable. |
Approval errors
Section titled “Approval errors”| Error | Meaning | Recovery |
|---|---|---|
ApprovalDeniedError | Approver clicked Deny. | Inspect e.details for the approver’s reason; surface to the requester. |
ApprovalExpiredError | Window elapsed; no decision made. | Re-issue the call or widen the policy’s window. |
ApprovalTimeoutError | SDK gave up waiting; row may still be pending. | Persist approval_id and re-poll later. |
ApprovalExecutionFailedError | Approved, but the eventual provider call failed. | Inspect e.details. Common cause: grant revoked between approval and execution. |
Connect flow errors
Section titled “Connect flow errors”| Error | Meaning |
|---|---|
ConnectDeniedError | User clicked Deny on the provider’s consent screen. |
ConnectConfigError | OAuth client is misconfigured (wrong redirect URI, invalid client ID/secret). Check the provider configuration in the portal. |
ConnectTimeoutError | User did not complete OAuth within the session lifetime. |
Recovering from missing-grant errors
Section titled “Recovering from missing-grant errors”When a request fails because the user hasn’t authorized the provider yet, the SDK exposes recovery context on the typed error so a re-consent flow can be driven without manual derivation:
NoDelegatedGrantErrorcarriesprovider_id/agent_id/app_user_id(camelCase in TypeScript).GrantNotFoundErrorcarriesprovider_id/agent_id/app_user_idwhen the raise was identity-mode.CredentialRevokedErrorcarriesprovider_id/app_user_id(noagent_id— credential revocation is grant-scoped, not agent-scoped).
Python
from alter_sdk import NoDelegatedGrantError
try: await app.request(provider="slack", user_token=jwt, url=..., method=...)except NoDelegatedGrantError as e: # 1. Mint a recovery Connect session using the error's context. session = await app.create_connect_session_for_error( e, allowed_origin="https://app.example.com", ) # 2. Surface the URL to the user. redirect_user(session.connect_url) # 3. Poll until consent completes. results = await app.poll_connect_session(session.session_token) # 4. Retry with the freshly-minted grant_id. response = await app.request(grant_id=results[0].grant_id, url=..., method=...)TypeScript
import { NoDelegatedGrantError } from "@alter-ai/alter-sdk";
try { await app.request(HttpMethod.GET, "...", { provider: "slack", userToken: jwt });} catch (e) { if (e instanceof NoDelegatedGrantError) { // 1. Mint a recovery Connect session. const session = await app.createConnectSessionForError(e, { allowedOrigin: "https://app.example.com", }); // 2. Surface the URL. redirectUser(session.connectUrl); // 3. Poll until consent completes. const results = await app.pollConnectSession(session.sessionToken); // 4. Retry. const response = await app.request(HttpMethod.GET, "...", { grantId: results[0].grantId, }); } else { // Re-throw every other error class — silently swallowing // NetworkError, BackendError, or programming bugs would hide // the real failure. throw e; }}When recovery context is available
Section titled “When recovery context is available”| Error | Identity-mode raise (carries context) | Direct-mode raise (context is None) |
|---|---|---|
NoDelegatedGrantError | Agent-runtime resolution with provider= + optional user_token= | n/a — always identity-mode |
GrantNotFoundError | Identity-mode lookup by (user, provider) failed | Caller passed an explicit grant_id= that doesn’t exist |
CredentialRevokedError | Token refresh hit a permanent failure (provider revoked, refresh token expired) | n/a — always carries a grant_id |
create_connect_session_for_error raises AlterValueError when recovery context isn’t available — for direct-mode GrantNotFoundError (stale grant_id), the SDK can’t infer which provider to re-consent to, so the developer must call create_connect_session directly with the right allowed_providers=.
What recovery does NOT cover
Section titled “What recovery does NOT cover”AmbiguousGrantError— surfacecandidates(each carries agrant_id, the universal disambiguator) to let the user or agent pick, then retry with the chosengrant_id. For the OAuth account flavor,account_identifiers+account=and, for the agent flavor,app_user_ids+user_token=are alternates — and the only usable disambiguators when account/user fields are empty are thegrant_ids incandidates. Minting a new session would be the wrong remediation here.