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.

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>" });
try {
await 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 RateLimitError) {
// A transient throttle. `scope` says WHICH ceiling was hit, which decides
// the remedy: spreading load across keys helps an `api_key` throttle and
// does nothing for an `organization` one.
console.warn(`Throttled on ${error.scope ?? "an unnamed ceiling"}`);
await new Promise((r) => setTimeout(r, (error.retryAfter ?? 1) * 1000));
return makeRequest();
}
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.”
  • BackendError — generic backend failure. details.status_code is populated when an HTTP status code is the proximate cause.
  • RestrictedGrantRequiresProxyError — the grant is restricted to the proxy call path; use proxyRequest().
  • 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 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 is not delegable, or a managed secret’s operator ceiling blocks re-delegable grants (HTTP 403 grant_not_delegable).
  • 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.
  • 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, 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.
  • 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 with details.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.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).
  • 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.
  • 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
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

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.

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.