Skip to content

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, and KeyAppNotFoundError require @alter-ai/alter-sdk>=0.23.1. Version 0.22.0 does not export them; upgrade before importing or checking instanceof against these classes. MemoryIsolationError, its code property and the MEMORY_ISOLATION_CODES vocabulary require @alter-ai/alter-sdk>=0.28.0. Version 0.27.0 and earlier do 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.

  • AlterSDKError — root of the hierarchy. Every other exception extends this.
  • AlterValueError — input validation failed at the SDK boundary. Distinct from BackendError so 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 of AlterSDKError, thrown locally by memoryScope(), ScopedMemoryStore and verifyIdentityAssertion(), never from an HTTP response. Branch on code (a MemoryIsolationCode, one of the exported MEMORY_ISOLATION_CODES); details.reason carries a coarse category for memory_assertion_invalid. See Errors for the code table.
  • BackendError — generic backend failure. details.status_code is populated when an HTTP status code is the proximate cause.
  • ServerFeatureUnsupportedError — extends BackendError; no extra fields. The Alter backend predates a feature the call used, so the SDK throws instead of returning a session that would not do what was asked. On createManagedSecretConnectSession() (App and Agent), details.feature names what is missing: managed_secret_connect_provider — the backend still requires templateSlug when only provider was sent, refuses an option it does not know (provider, agent, requestedGrant, grantPolicy, direct, repairGrantId), answered a session without the path a current backend always reports (it ignored those options), 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.getAgent(agentId).createManagedSecretConnectSession({ templateSlug, ... }) or the App method with templateSlug and delegatedAgentId — without delegable.
  • RestrictedGrantRequiresProxyError — the grant is proxy-only, so request() (raw credential release) refuses it; use proxyRequest(). 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.
  • SiblingLabelConflictError — the sibling label is already in use on this connection; pick another label.
  • CredentialIdRequiredError — the provider holds several developer credentials and createManagedSecretGrant() named none (HTTP 409 credential_id_required); retry with one of credentials.
  • EndUserCredentialNotAssignableError — credentialId 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).
  • ReAuthRequiredError — the user must re-authorize via Alter Connect. Parent class of the grant-state errors below (GrantExpiredError, GrantRevokedError, GrantDeletedError, CredentialRevokedError), so one instanceof catch covers every “send the user back through Connect” condition.
  • GrantExpiredError — the grant’s TTL has elapsed. Exposes providerId, agentId, appUserId, populated only on the delegation-path expiry (a TTL-lapsed delegation chain); undefined on 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 subsequent App-side call that addresses the revoked grant. Agent callers — and calls addressing a delegated (child) grant — never receive this class: for them a revoked grant surfaces as the reason-neutral GrantDeletedError, 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 the Agent path). Re-authorization issues a new grant id.
  • GrantNotFoundError
  • AgentDelegationMissingError — extends GrantNotFoundError (so an instanceof GrantNotFoundError catch still fires). Thrown on the agent path when request(method, url, { grantId }) / proxyRequest({ grantId }) hits a grant_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 (omit grantId). The App/operator path is unaffected.
  • GrantNotDelegableError — extends BackendError. Thrown 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). detailReason names which: no_consenting_user, holder_not_delegable, ceiling_off, no_onward_hop or not_requested (null when the backend sent none).
  • CredentialRevokedError
  • AmbiguousGrantError — multiple grants match the identity + provider tuple. Pass account or use direct grantId mode.
  • NoDelegatedGrantError — the calling agent has no delegation for the resolved user on this provider. Recoverable via createConnectSessionForError().
  • PolicyViolationError — backend policy denial.
  • StepUpRequiredError — extends PolicyViolationError. A content_match step-up obligation requires a fresher user session.
  • RedactDischargeFailedError — extends PolicyViolationError. 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. Exposes retryAfter (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 be undefined). 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: wait retryAfter seconds and retry the same request (retryAfter is capped at 3,600 seconds — one hour, above the platform’s legitimate retry guidance). Also exposes scope (which ceiling was hit — a plain string, not a closed union, so a ceiling added later stays parseable; the known values are exported as RATE_LIMIT_SCOPE_VALUES, currently "ip", "api_key" and "organization", ordered outermost-first), limit, and windowSeconds. Each of those three may be undefined — 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, so scope, limit and windowSeconds are undefined, retryAfter is set only from a Retry-After header, and details is { status_code: 429 }.
  • ProviderAPIError — provider returned a 4xx/5xx that is not a scope or credential-rejection failure.
  • ScopeReauthRequiredError — provider returned 403 insufficient_scope, or the backend already knew the grant’s scopes are drifted. Exposes missingScopes and providerId.
  • ProviderUnauthorizedError — provider returned 401: the credential was revoked, expired, or otherwise invalidated provider-side. Exposes grantId, providerId, statusCode, responseBody. Recovery: 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 repairGrantId. 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.
  • 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.), including provider_configuration_unavailable, shared_dev_credential_unavailable, and managed_oauth_scope_approval_required when fresh authorization cannot start. createManagedSecretConnectSession() also throws it when the session cannot be created until an administrator or the application changes something (retrying does not help); details.error names it: 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 templateSlug); idp_signin_not_configured (HTTP 409, details.retryable === 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 templateSlug-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.getAgent(), 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.
  • ConnectTimeoutError — Alter received no completion callback before the local deadline or before a session observed pending expired. details.reason is "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.
  • ApprovalError — base class.
  • ApprovalDeniedError
  • ApprovalExpiredError
  • ApprovalTimeoutError — local wait elapsed before any decision. Original transient (when present) is preserved on .cause.
  • ApprovalExecutionFailedError
  • AgentError — base class.
  • InvalidKeyError
  • KeyExpiredError
  • KeyRotationExpiredError
  • KeyInactiveError
  • KeyAppNotFoundError
  • AgentNotFoundError
  • CallerAgentMismatchError
  • CallerAgentResolutionError
  • AgentNameExistsError
  • AgentConcurrentUpdateError — the agent changed after it was read. Fetch it again, recompute the full desired scope allowlist, and retry with the fresh version.
  • AgentInactiveError
  • AgentRevokedError
  • MeRequiresAgentKeyError — agent.me() was called through an impersonating Agent returned by App.getAgent(), so the request still used an application key rather than a genuine agent key.
  • KeyRevokedError
  • KeyAlreadyRevokedError
  • KeyNotFoundError
  • LastActiveKeyError — would revoke the agent’s only remaining active key. Pass force: true to override.
  • AgentKeyLimitError — the mint would take the agent past its per-agent active-key cap. Revoke a key it no longer uses, then retry.
  • AgentCannotMintSubagentsError
  • IdempotencyKeyBodyMismatchError
  • IdempotencyKeyAgentRevokedError
  • IdempotencyKeyAgentInactiveError
  • NetworkError — TCP / DNS / socket failure.
  • TimeoutError — extends NetworkError. Local or remote timeout.

Every AlterSDKError carries details: Record<string, unknown>. toString() returns only the safe message and never stringifies details. Common detail keys:

KeyTypeDescription
status_codenumberHTTP status code from the backend or provider.
grant_idstringGrant the call resolved to (when applicable).
provider_idstringProvider slug (when applicable).
method, urlstringHTTP method and pre-injection URL.
reasonstringBackend-reported reason code.
retry_afternumberSeconds 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.

The following public fields supplement message, name, and details:

ClassFields
GrantExpiredErrorproviderId, agentId, appUserId (string | undefined)
GrantRevokedErrorgrantId: string | undefined
CredentialRevokedErrorgrantId, providerId, appUserId (string | undefined)
GrantNotFoundErrorproviderId, agentId, appUserId (string | undefined)
AmbiguousGrantErrorproviderId, 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
NoDelegatedGrantErrorproviderId, agentId, appUserId (string | undefined)
PolicyViolationErrorpolicyError: string | undefined
StepUpRequiredErrormaxSessionAgeSeconds: number | undefined; inherited policyError is "step_up_required"
RedactDischargeFailedErrorno extra fields; inherited policyError is "redact_discharge_failed"
SiblingLabelConflictErrorlabel: string | null
CredentialIdRequiredErrorcredentials: CredentialChoice[] — every developer credential the provider holds
EndUserCredentialNotAssignableErrorno extra fields
InsufficientScopeErrorrequired, granted, missing, scopeVersion, currentScopeVersion, scopeVersionMismatch, documentationUrl; aliases requiredScopes, grantedScopes, docsUrl
TokenRefreshInProgressErrorgrantId: string | undefined
QuotaExceededErrorretryAfter: number | undefined
ProviderAPIErrorstatusCode: number | undefined, responseBody: string | undefined
ScopeReauthRequiredErrorgrantId, providerId, missingScopes; inherits provider response fields
ProviderUnauthorizedErrorgrantId, providerId; inherits provider response fields
ApprovalError and subclassesapprovalId: string | null
AgentError and subclassescode: string, hint?: string

CredentialIdRequiredError.credentials contains CredentialChoice entries with credentialId (what createManagedSecretGrant(id, { credentialId }) takes) and label.

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.

Version compatibility: BlockedDestinationError, and RateLimitError for a non-JSON 429, require @alter-ai/alter-sdk>=0.28.0. Version 0.27.0 and earlier throw 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.