Reference
Errors
Exception hierarchy exported from @alter-ai/alter-sdk.
Every public exception thrown by the SDK extends AlterSDKError. The hierarchy mirrors the Python SDK one-for-one — same class names, same semantics, same fields.
Version compatibility: this page tracks the latest published SDK line.
CallerAgentMismatchError,CallerAgentResolutionError,KeyExpiredError,KeyRotationExpiredError,KeyInactiveError, andKeyAppNotFoundErrorrequire@alter-ai/alter-sdk>=0.23.1. Version 0.22.0 does not export them; upgrade before importing or checkinginstanceofagainst these classes.MemoryIsolationError, itscodeproperty and theMEMORY_ISOLATION_CODESvocabulary are unreleased: they ship in the next release after 0.27.0, which does not export them.
For the canonical error code → exception class mapping and recovery guidance, see Errors.
import { NoDelegatedGrantError, QuotaExceededError, RateLimitError, ScopeReauthRequiredError,} from "@alter-ai/alter-sdk";
const makeRequest = () => app.request("GET", url, { provider: "<provider-id>" });
// Bounded retries on a throttle: honour `retryAfter` when the response carried// one, otherwise back off exponentially; add jitter so callers spread out.async function withRateLimitRetry<T>( call: () => Promise<T>, maxAttempts = 5,): Promise<T> { for (let attempt = 1; ; attempt++) { try { return await call(); } catch (error) { if (!(error instanceof RateLimitError) || attempt >= maxAttempts) { throw error; } // `scope` says WHICH ceiling was hit: spreading load across keys helps // an `api_key` throttle and does nothing for an `organization` one. It is // undefined for a non-JSON 429 from an intermediary. console.warn(`Throttled on ${error.scope ?? "an unidentified limiter"}`); const waitSeconds = error.retryAfter ?? Math.min(60, 2 ** attempt); const jitterSeconds = Math.random() * waitSeconds * 0.2; await new Promise((r) => setTimeout(r, (waitSeconds + jitterSeconds) * 1000), ); } }}
try { await withRateLimitRetry(makeRequest);} catch (error) { if (error instanceof NoDelegatedGrantError) { const session = await app.createConnectSessionForError(error); redirectUser(session.connectUrl); return; } if (error instanceof ScopeReauthRequiredError) { // The user must re-authorize with wider provider scopes. console.warn(`Missing scopes: ${error.missingScopes?.join(", ")}`); } if (error instanceof QuotaExceededError) { // Request-quota policy denial — the SDK never auto-retries it. await new Promise((r) => setTimeout(r, (error.retryAfter ?? 60) * 1000)); return makeRequest(); } throw error;}Every name imported above is used by the snippet, so it compiles as-is under
noUnusedLocals. The classes documented below that it does not branch on
(AlterSDKError, GrantNotFoundError, GrantNotDelegableError, …) are
deliberately not imported here — add them when you add a branch for them.
Class hierarchy
Section titled “Class hierarchy”AlterSDKError— root of the hierarchy. Every other exception extends this.AlterValueError— input validation failed at the SDK boundary. Distinct fromBackendErrorso callers can branch on “I passed something bad” vs “the backend said no.”MemoryIsolationError— the private-memory identity or partition could not be verified. A direct child ofAlterSDKError, thrown locally bymemoryScope(),ScopedMemoryStoreandverifyIdentityAssertion(), never from an HTTP response. Branch oncode(aMemoryIsolationCode, one of the exportedMEMORY_ISOLATION_CODES);details.reasoncarries a coarse category formemory_assertion_invalid. See Errors for the code table.
Backend errors
Section titled “Backend errors”BackendError— generic backend failure.details.status_codeis populated when an HTTP status code is the proximate cause.ServerFeatureUnsupportedError— extendsBackendError; no extra fields. The Alter backend predates a feature the call used:agent.createManagedSecretConnectSession()against a backend that still requiresdelegated_agent_id(its request-validation 422 names that field as missing), ordelegable: trueoncreateManagedSecretConnectSessionwhen the backend did not echodelegable_requested, or echoed it asfalse. The SDK throws instead of returning a session that would not do what was asked.details.featurenames what is missing:agent_managed_secret_connect_sessionormanaged_secret_connect_delegable. Retrying against the same backend cannot help: until it supports the feature, request the session with an app key —app.getAgent(agentId).createManagedSecretConnectSession(...)or the App method — withoutdelegable.RestrictedGrantRequiresProxyError— the grant is restricted to the proxy call path; useproxyRequest().SiblingLabelConflictError— the sibling label is already in use on this connection; pick another label.ReAuthRequiredError— the user must re-authorize via Alter Connect. Parent class of the grant-state errors below (GrantExpiredError,GrantRevokedError,GrantDeletedError,CredentialRevokedError), so oneinstanceofcatch covers every “send the user back through Connect” condition.
Grant state
Section titled “Grant state”GrantExpiredError— the grant’s TTL has elapsed. ExposesproviderId,agentId,appUserId, populated only on the delegation-path expiry (a TTL-lapsed delegation chain);undefinedon the root-path TTL expiry.GrantRevokedError— the grant was explicitly revoked (the end user revoked it in the Wallet or disconnected the connection, or an administrator revoked it). Raised on every subsequentApp-side call that addresses the revoked grant.Agentcallers — and calls addressing a delegated (child) grant — never receive this class: for them a revoked grant surfaces as the reason-neutralGrantDeletedError, so a delegated agent cannot probe whether its access was deliberately revoked.GrantDeletedError— the grant existed but is permanently gone with no more specific terminal signal (for example, a TTL-expired grant addressed again, a grant whose underlying credential was torn down, or any revoked grant addressed from theAgentpath). Re-authorization issues a new grant id.GrantNotFoundErrorAgentDelegationMissingError— extendsGrantNotFoundError(so aninstanceof GrantNotFoundErrorcatch still fires). Thrown on the agent path whenrequest(method, url, { grantId })/proxyRequest({ grantId })hits agrant_not_found— the grant is not delegated to this agent, or a user/base grant id was passed where the agent’s own delegation id (listGrants) was required. Recover by delegating the agent through Connect, or resolve by provider (omitgrantId). TheApp/operator path is unaffected.GrantNotDelegableError— extendsBackendError. Thrown byagent.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), ordelegable: trueasks for a child that the depth ceiling leaves no further hop (HTTP 403grant_not_delegable).detailReasonnames which:no_consenting_user,holder_not_delegable,ceiling_off,no_onward_hopornot_requested(nullwhen the backend sent none).CredentialRevokedError
Identity resolution
Section titled “Identity resolution”AmbiguousGrantError— multiple grants match the identity + provider tuple. Passaccountor use directgrantIdmode.NoDelegatedGrantError— the calling agent has no delegation for the resolved user on this provider. Recoverable viacreateConnectSessionForError().
Authorization
Section titled “Authorization”PolicyViolationError— backend policy denial.StepUpRequiredError— extendsPolicyViolationError. Acontent_matchstep-up obligation requires a fresher user session.RedactDischargeFailedError— extendsPolicyViolationError. The request was denied because a redact obligation could not be applied safely.InsufficientScopeError— API key lacks the required scope.TokenRefreshInProgressError— concurrent refresh in flight; retry shortly.QuotaExceededError— a request-quota policy window is exhausted. ExposesretryAfter(seconds until the window resets, 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 beundefined). A policy denial, not a transient outage — the SDK never auto-retries it.RateLimitError— the platform rate limit for the API key (or the organization-wide aggregate, when configured) is exhausted. A transient throttle, not a denial: waitretryAfterseconds and retry the same request (retryAfteris capped at 3,600 seconds — one hour, above the platform’s legitimate retry guidance). Also exposesscope(which ceiling was hit — a plainstring, not a closed union, so a ceiling added later stays parseable; the known values are exported asRATE_LIMIT_SCOPE_VALUES, currently"ip","api_key"and"organization", ordered outermost-first),limit, andwindowSeconds. Each of those three may beundefined— not every response carrying this error code supplies them, so branch on a missing value rather than assuming one. Also thrown for a non-JSON 429 from an intermediary such as an edge provider: the limiting layer is unknown, soscope,limitandwindowSecondsareundefined,retryAfteris set only from aRetry-Afterheader, anddetailsis{ status_code: 429 }.
Provider
Section titled “Provider”ProviderAPIError— provider returned a 4xx/5xx that is not a scope or credential-rejection failure.ScopeReauthRequiredError— provider returned 403insufficient_scope, or the backend already knew the grant’s scopes are drifted. ExposesmissingScopesandproviderId.ProviderUnauthorizedError— provider returned 401: the credential was revoked, expired, or otherwise invalidated provider-side. ExposesgrantId,providerId,statusCode,responseBody. Recovery: re-authorize an OAuth grant via a new Connect session, or update the stored managed secret. A 401 whoseWWW-Authenticatechallenge carrieserror="insufficient_scope"(RFC 6750 — the credential is still valid, it just lacks a scope) surfaces as a genericProviderAPIErrorinstead.
Connect flow
Section titled “Connect flow”ConnectFlowError— generic flow failure, including an unavailable/expired token on the first poll, an unknown terminal state, or a total usage-limit application failure.ConnectDeniedError— user clicked Deny.ConnectConfigError— provider configuration issue (invalid redirect URI, unknown client, etc.), includingprovider_configuration_unavailable,shared_dev_credential_unavailable, andmanaged_oauth_scope_approval_requiredwhen fresh authorization cannot start.createManagedSecretConnectSessionalso throws it withdetails.error === "idp_signin_not_configured"(HTTP 409,details.retryable === false) when the application’s identity provider cannot sign users in to approve the delegation.details.details.reasonsays what to change:missing_client_idormissing_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 issuerhttps://<slug>.authkit.app). It also throws it withdetails.error === "no_eligible_grants"(HTTP 409) when the user holds no credential the delegation could use;details.details.reasonisno_grant,grant_expired,delegation_depth_exceeded,group_source_not_allowedoragent_not_allowlisted(see the reasons). Only a key bound to no managed agent receivesno_eligible_grants: an app key, including anAgentfromApp.getAgent(), or an agent-typedalter_ak_…key minted under API Keys. A managed agent’s own key never does.ConnectTimeoutError— Alter received no completion callback before the local deadline or before a session observed pending expired.details.reasonis"poll_deadline_elapsed"or"session_expired"; without a callback, an exact provider-side error is unknowable. 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.
Approvals
Section titled “Approvals”ApprovalError— base class.ApprovalDeniedErrorApprovalExpiredErrorApprovalTimeoutError— local wait elapsed before any decision. Original transient (when present) is preserved on.cause.ApprovalExecutionFailedError
Managed agents
Section titled “Managed agents”AgentError— base class.InvalidKeyErrorKeyExpiredErrorKeyRotationExpiredErrorKeyInactiveErrorKeyAppNotFoundErrorAgentNotFoundErrorCallerAgentMismatchErrorCallerAgentResolutionErrorAgentNameExistsErrorAgentConcurrentUpdateError— the agent changed after it was read. Fetch it again, recompute the full desired scope allowlist, and retry with the freshversion.AgentInactiveErrorAgentRevokedErrorMeRequiresAgentKeyError—agent.me()was called through an impersonatingAgentreturned byApp.getAgent(), so the request still used an application key rather than a genuine agent key.KeyRevokedErrorKeyAlreadyRevokedErrorKeyNotFoundErrorLastActiveKeyError— would revoke the agent’s only remaining active key. Passforce: trueto override.AgentKeyLimitError— the mint would take the agent past its per-agent active-key cap. Revoke a key it no longer uses, then retry.AgentCannotMintSubagentsErrorIdempotencyKeyBodyMismatchErrorIdempotencyKeyAgentRevokedErrorIdempotencyKeyAgentInactiveError
Network
Section titled “Network”NetworkError— TCP / DNS / socket failure.TimeoutError— extendsNetworkError. Local or remote timeout.
Common fields
Section titled “Common fields”Every AlterSDKError carries details: Record<string, unknown>. toString() returns only the safe message and never stringifies details. Common detail keys:
| Key | Type | Description |
|---|---|---|
status_code | number | HTTP status code from the backend or provider. |
grant_id | string | Grant the call resolved to (when applicable). |
provider_id | string | Provider slug (when applicable). |
method, url | string | HTTP method and pre-injection URL. |
reason | string | Backend-reported reason code. |
retry_after | number | Seconds to wait before retrying — a quota window reset or a rate-limit backoff. QuotaExceededError and RateLimitError also expose it as the typed retryAfter field (may be undefined). On both, the typed field prefers the Retry-After response header over this body value and caps the result — at 3,600 seconds for RateLimitError, at 2,678,400 (31 days) for QuotaExceededError, whose windows are calendar periods — so the two can differ. |
Typed fields
Section titled “Typed fields”The following public fields supplement message, name, and details:
| Class | Fields |
|---|---|
GrantExpiredError | providerId, agentId, appUserId (string | undefined) |
GrantRevokedError | grantId: string | undefined |
CredentialRevokedError | grantId, providerId, appUserId (string | undefined) |
GrantNotFoundError | providerId, agentId, appUserId (string | undefined) |
AmbiguousGrantError | providerId, accountIdentifiers, accountWasProvided, appUserIds, grantIds, candidates, candidatesTruncated — when candidatesTruncated is true every disambiguator here is a bounded sample, so an absence from any of them proves nothing |
NoDelegatedGrantError | providerId, agentId, appUserId (string | undefined) |
PolicyViolationError | policyError: string | undefined |
StepUpRequiredError | maxSessionAgeSeconds: number | undefined; inherited policyError is "step_up_required" |
RedactDischargeFailedError | no extra fields; inherited policyError is "redact_discharge_failed" |
SiblingLabelConflictError | label: string | null |
InsufficientScopeError | required, granted, missing, scopeVersion, currentScopeVersion, scopeVersionMismatch, documentationUrl; aliases requiredScopes, grantedScopes, docsUrl |
TokenRefreshInProgressError | grantId: string | undefined |
QuotaExceededError | retryAfter: number | undefined |
ProviderAPIError | statusCode: number | undefined, responseBody: string | undefined |
ScopeReauthRequiredError | grantId, providerId, missingScopes; inherits provider response fields |
ProviderUnauthorizedError | grantId, providerId; inherits provider response fields |
ApprovalError and subclasses | approvalId: string | null |
AgentError and subclasses | code: string, hint?: string |
AmbiguousGrantError.candidates contains GrantCandidate entries with grantId, label, accountIdentifier, and accountDisplayName. The account fields are nullable.
Recover deliberately, never by taking the first candidate:
try { response = await app.request(HttpMethod.GET, url, { provider: "<provider-id>", userToken: userJwt });} catch (e) { if (!(e instanceof AmbiguousGrantError)) throw e; const grantId = chooseGrant(e.candidates); // the user's choice, persisted on their profile response = await app.request(HttpMethod.GET, url, { grantId, userToken: userJwt });}For the typed exception fields and recovery flows for each code, see Errors.
Destination refusal
Section titled “Destination refusal”Version compatibility:
BlockedDestinationError, andRateLimitErrorfor a non-JSON 429, require an@alter-ai/alter-sdkrelease after 0.27.0. Version 0.27.0 and earlier throw 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.