Reference
Connect & Grants
Connect sessions, grant lifecycle, delegation revoke, managed-secret grants, HITL approvals.
This page covers every surface for getting a credential into the system and managing its lifecycle: the OAuth provider catalog, Connect sessions (user-consent flows), grant CRUD, agent-delegation revoke, managed-secret grant creation, and human-in-the-loop approvals.
// Provider catalogconst catalog = await app.oauthProviders.list();
// Connect flowsconst results = await app.connect({ providers: ["<provider-id>"] });const session = await app.createConnectSession({ allowedProviders: ["<provider-id>"] });const grants = await app.pollConnectSession(session.sessionToken);const recover = await app.createConnectSessionForError(caughtError);const msSess = await app.createManagedSecretConnectSession({ /* ... */ });
// End-user sign-in (IDP login)const auth = await app.authenticate(); // CLI convenienceconst authSession = await app.createAuthSession(); // headless / remote userconst result = await app.pollAuthSession(authSession.sessionToken);
// Grantsconst page = await app.listGrants({ limit: 50 });await app.revokeGrant(grantId, { reason: "rotation" });const grant = await app.createManagedSecretGrant("ms_…", { principal });const sibling = await app.mintGrant(grantId, { label: "read-only" });
// Delegationsawait app.revokeDelegation(grantId, agentId); // operatorawait agent.revokeDelegation(grantId); // agent self-revoke
// Approvalsconst final = await app.awaitApproval(approvalId, { timeoutMs: 600_000 });const status = await app.getApprovalStatus(approvalId);oauthProviders.list
Section titled “oauthProviders.list”Fetch the OAuth provider catalog: the connectable providers configured for the platform and, per provider, the scopes you can request. Use it to drive provider pickers and scope selection instead of hardcoding scope strings.
async list(options?: { forceRefresh?: boolean }): Promise<OAuthProviderCatalog>| Option | Type | Default | Description |
|---|---|---|---|
forceRefresh | boolean | false | Bypass the in-process cache and re-fetch. |
Results are cached in-process for 5 minutes. The catalog lists only active (connectable) providers. Available on both App and Agent as app.oauthProviders / agent.oauthProviders.
Returns OAuthProviderCatalog — a providers record keyed by id, plus getDefaultScopes(provider) / getRequiredScopes(provider) helpers. Throws BackendError on a malformed catalog response. Requires the providers:read scope.
const catalog = await app.oauthProviders.list();const provider = catalog.providers["<providerId>"];console.log(provider.displayName, provider.defaultScopes);
// Pre-fill a Connect flow with the provider's default scopes.const scopes = catalog.getDefaultScopes("<providerId>");createConnectSession
Section titled “createConnectSession”Mint a Connect session URL. The application then surfaces the URL to the user — popup, redirect, mobile webview, chat message, etc.
async createConnectSession( options?: CreateConnectSessionOptions,): Promise<ConnectSession>| Option | Type | Description |
|---|---|---|
allowedProviders | string[] | Restrict to specific provider IDs (for example, ["<provider-id>"]). When omitted the user can pick any configured provider. |
returnUrl | string | URL the browser redirects to after consent (mobile / single-page flows). |
allowedOrigin | string | Origin allowed for the postMessage completion event (popup flow). |
metadata | { ipAddress?: string; userAgent?: string } | Forwarded to the audit row. |
grantPolicy | GrantPolicyInput | Optional absolute expiry and TTL bounds the user picks from on the consent screen. TTL fields are seconds. |
requiredScopes | Record<string, string[]> | Per-provider scope ceiling. The OAuth URL is built with these scopes (must be a subset of the application’s Dev Portal configuration). |
agent | string | Agent UUID or managed-agent name. When set, on approval the SDK returns a delegation grant that the agent uses as grantId on request(). |
requestedGrant | RequestedGrant | Sibling-grant request (multi-grant model). The grant created by this session carries the required label (the sibling address — resolution key is provider + label) and, when set, the method/endpoint restrictions, which make it proxy-only. Unknown keys throw AlterValueError at the SDK boundary. |
delegable | boolean | Only meaningful with agent. When true, the resulting (first-hop) grant may itself be re-delegated by the recipient agent via agent.delegate(). Onward delegation is opt-in at every hop — leave it false (the default) for a leaf grant the recipient cannot pass on. |
scopeConstraint | string[] | Only meaningful with agent. Narrow the delegated agent’s first-hop grant to a subset of the credential’s provider scopes (least privilege). Omit for full credential scope. Provide a non-empty array of at most 100 concrete scope strings, each ≤ 512 characters and a valid RFC 6749 scope-token (printable ASCII — no spaces, " or \; one atom per scope); "*", empty strings, and an empty array are rejected. A scope-narrowed grant is proxy-only — call it with proxyRequest(). If the constraint admits nothing against a provider’s requiredScopes ceiling, the session still succeeds and ConnectSession.scopeConstraintWarnings carries one diagnostic per affected provider. |
userToken | string | Per-call user JWT. Overrides userTokenGetter; omit both for an app-scoped/headless session. |
switchAccount | boolean | Force the full provider authorization flow, skipping the already-connected fast path — the programmatic equivalent of the consent screen’s “Use a different account” link. Set it when the flow’s purpose is connecting a different account for a provider the application already holds a connection to. Default false (routine reconnects keep the one-click fast path). Must be a boolean — a non-boolean throws AlterValueError at the SDK boundary. |
allowUserPolicyRules | boolean | Whether the end user may set their own usage limits — policy rules (deny, require-approval, time window, request quota, or operation-parameter content rules) — in the Connect popup while authorizing. The limits are applied as part of authorizing it — if they cannot be applied, the connection is revoked rather than left in place without them. Default true; set false to hide the policy step. Must be a boolean — a non-boolean throws AlterValueError at the SDK boundary. |
RequestedGrant and RequestedGrantRestrictions are exported from the package root:
interface RequestedGrant { label: string; restrictions?: RequestedGrantRestrictions | null;}
interface RequestedGrantRestrictions { allowedMethods?: string[]; allowedEndpoints?: string[];}Returns ConnectSession — connectUrl is what the user opens; sessionToken is what you pass to pollConnectSession().
const session = await app.createConnectSession({ allowedProviders: ["<provider-id>"], allowedOrigin: "https://app.example.com", grantPolicy: { maxTtlSeconds: 30 * 24 * 3600, defaultTtlSeconds: 7 * 24 * 3600 },});
window.open(session.connectUrl, "alter-connect", "popup");const grants = await app.pollConnectSession(session.sessionToken);createManagedSecretConnectSession
Section titled “createManagedSecretConnectSession”Mint a Connect session for the user → agent delegation flow on a managed secret. The user consents to the named agent using the user’s existing managed-secret access.
async createManagedSecretConnectSession( options: CreateManagedSecretConnectSessionOptions,): Promise<ManagedSecretConnectSession>| Option | Type | Description |
|---|---|---|
templateSlug | string | Canonical managed-secret template slug in kebab-case. Required. |
delegatedAgentId | string | UUID of the agent being authorized. Required. |
userToken | string | IDP JWT identifying the consenting user. Required. |
requestedTtlSeconds | number | Optional positive integer caller-suggested TTL in seconds. Capped by the per-secret policy, source credential expiry, and 90 days; omit it to use the selected secret’s policy and default cap. |
delegatedAgentName | string | Display name shown on the consent screen. Defaults to the agent’s stored display name. |
allowedOrigin | string | Origin allowed for the postMessage event. |
returnUrl | string | URL for the mobile redirect flow. |
allowUserPolicyRules | boolean | Whether the consenting user may add self-scoped, narrowing-only usage limits in the hosted approval flow. Defaults to true; set false to hide the policy step. The rules are attached atomically to the delegated managed-secret grant, and approval fails if they cannot be applied. A non-boolean throws AlterValueError before any request. |
Returns ManagedSecretConnectSession with the session URL and polling metadata; it does not contain a delegation ID. The user opens connectUrl. After approval, the hosted flow delivers delegation_id in the postMessage payload or returnUrl query, and the agent passes that value as grantId on request().
App-only method.
pollConnectSession
Section titled “pollConnectSession”Block until a session reaches a terminal state. Use this when application code minted the session itself and needs to wait for completion.
async pollConnectSession( sessionToken: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<ConnectResult[]>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 (5 min) | Positive, finite maximum time to wait. Milliseconds. An explicit value is honored; it does not shorten the server session. Elapsed time uses a monotonic clock. |
pollIntervalMs | number | 2_000 | Positive, finite time between polls. Milliseconds. |
Returns one ConnectResult per provider the user completed. On partial completion, every result carries the same typed failedGrants array (providerId, reason, message) for providers whose grants were revoked because the selected usage limits could not be applied. The array is empty for full success; unknown reason strings are preserved.
Throws:
AlterValueError— blank/non-string token or non-finite/non-positive polling control; rejected before any request.ConnectTimeoutError— the local deadline elapsed (details.reason === "poll_deadline_elapsed"), or a session observed pending expired before Alter received a callback ("session_expired").ConnectDeniedError— the user clicked Deny.ConnectConfigError— provider configuration issue (invalid redirect URI, unknown client, etc.).ConnectFlowError— other failure, including an unavailable/expired token on the first poll (the API deliberately does not reveal whether a session existed) or a completed flow where every authorized grant was revoked because the selected usage limits could not be applied. In the latter case,error.details.failed_grantscontains the typed failures in snake_case wire form.BackendError— malformed terminal error fields or another malformed backend response.
createConnectSessionForError
Section titled “createConnectSessionForError”Recover from a typed credential failure. Pass the caught exception and the method extracts the provider, the delegated agent (when present), and builds a re-authorization session.
async createConnectSessionForError( error: NoDelegatedGrantError | GrantNotFoundError | CredentialRevokedError, options?: { allowedOrigin?: string; returnUrl?: string; metadata?: { ipAddress?: string; userAgent?: string }; grantPolicy?: GrantPolicyInput; requiredScopes?: Record<string, string[]>; userToken?: string; },): Promise<ConnectSession>try { await app.request("GET", url, { provider: "<provider-id>" });} catch (error) { if (error instanceof NoDelegatedGrantError) { const session = await app.createConnectSessionForError(error, { allowedOrigin: "https://app.example.com", }); redirectUser(session.connectUrl); const results = await app.pollConnectSession(session.sessionToken); // Retry the original call. } else { // Re-throw anything you don't explicitly recover from — never silently // swallow an unexpected error (e.g. a network failure or provider error). throw error; }}Throws AlterValueError when the typed error has no providerId context (rare — only happens when the original failure was direct-mode against a stale grantId).
connect
Section titled “connect”The all-in-one headless flow. Mints a session, opens the user’s default browser, polls until done, and returns the resulting grants.
async connect(options: ConnectOptions): Promise<ConnectResult[]>| Option | Type | Default | Description |
|---|---|---|---|
providers | string[] | — | Restrict to specific providers. |
timeout | number | 300_000 | Positive, finite maximum wait. Milliseconds. An explicit value is honored; it does not shorten the server session. Elapsed time uses a monotonic clock. |
pollInterval | number | 2_000 | Positive, finite time between polls. Milliseconds. |
openBrowser | boolean | true | When false, prints the URL instead of launching the browser. |
grantPolicy | GrantPolicyInput | — | Optional absolute expiry and TTL bounds passed to the Connect UI. TTL fields are seconds. |
allowUserPolicyRules | boolean | true | Whether the end user may set their own usage limits — policy rules (deny, require-approval, time window, request quota, or operation-parameter content rules) — in the Connect popup while authorizing. Forwarded to the Connect session. The limits are applied as part of authorizing the connection — if they cannot be applied, the connection is revoked rather than left in place without them. Set false to hide the policy step. Must be a boolean — a non-boolean throws AlterValueError at the SDK boundary. |
const results = await app.connect({ providers: ["<provider-id>"], timeout: 10 * 60_000,});console.log(`Connected ${results.length} provider(s)`);for (const failed of results[0]?.failedGrants ?? []) { console.warn(`Not connected: ${failed.providerId}: ${failed.message}`);}Use this for CLI tools and scripts. For embedded UI flows, prefer createConnectSession() + pollConnectSession() so application code controls the rendering.
The browser launch uses the optional open package when the host application installs it. It is not a declared SDK peer dependency. When it is unavailable, connect() falls back to printing the URL.
Throws:
AlterValueError—grantPolicyis malformed orallowUserPolicyRulesis not a boolean.ConnectTimeoutError— Alter received no completion callback before the local deadline or observed session expiry; inspectdetails.reason.ConnectDeniedError/ConnectConfigError— denial or provider configuration failure.ConnectFlowError— another terminal failure, including when every authorized grant was revoked because selected usage limits could not be applied. The total-failure error carriesdetails.failed_grants; partial failures are returned asConnectResult.failedGrants.
authenticate
Section titled “authenticate”Open the application’s configured IDP login page in the user’s default browser, poll until the user authenticates, and return their IDP JWT.
async authenticate(options?: { timeoutMs?: number }): Promise<AuthResult>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 | Maximum wait. Milliseconds. |
Returns AuthResult with userToken and userInfo.
App-only method.
createAuthSession
Section titled “createAuthSession”The split, headless counterpart to authenticate(). Mints a sign-in session and returns the IDP authUrl without opening a browser or mutating the instance. Hand authUrl to a user on any channel (a chat message, an MCP client, a printed link), then poll with pollAuthSession(). Because it installs no userTokenGetter, it is safe on a shared App that resolves a JWT per request.
async createAuthSession(): Promise<AuthSession>Returns AuthSession with sessionToken, authUrl, expiresIn, and expiresAt. sessionToken is persistable: a worker can store it, poll in the background, and resume after a restart. Throws AlterSDKError (no IDP configured) or BackendError (malformed response). Requires the idp_users:write scope. App-only — agents do not start user logins.
const session = await app.createAuthSession();// Send `session.authUrl` to the user; never log it (it carries the session token).return { signInUrl: session.authUrl, session: session.sessionToken };pollAuthSession
Section titled “pollAuthSession”The polling half of the link-based flow. Resolves when the user finishes IDP login, returning their JWT. Like createAuthSession(), it installs no userTokenGetter — the caller decides what to do with the token.
async pollAuthSession( sessionToken: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<AuthResult>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 | Maximum wait. Milliseconds. The Python SDK’s equivalent takes seconds. |
pollIntervalMs | number | 2_000 | Milliseconds between polls. |
Transient network blips and non-200 responses are retried until the deadline; only a terminal IDP error, an expired session, or the timeout ends the loop.
Returns AuthResult with userToken and userInfo. Throws AlterValueError (blank sessionToken), ConnectTimeoutError (deadline; details.reason is "poll_deadline_elapsed"), or the matching typed subclass on a permanent backend failure. Requires the idp_users:read scope.
const session = await app.createAuthSession();const result = await app.pollAuthSession(session.sessionToken, { timeoutMs: 600_000 });listGrants
Section titled “listGrants”Return the calling principal’s accessible grants, paginated.
async listGrants( options?: AppListGrantsOptions | AgentListGrantsOptions,): Promise<UnifiedGrantListResult>Available on both App and Agent. The backend dispatches by principal kind:
- App — every grant the application owns: OAuth and managed-secret, across all principal kinds (user, group, system, agent). With
userTokenGetterconfigured (orendUserTokenpassed), the list narrows to that end user — their own grants plus group grants they are a live member of. - Agent — only what the agent can reach: OAuth grants delegated to it, plus managed-secret grants it owns or that are delegated to it (
accessViamarksownershipvs delegation). Other agents’ grants, non-delegating users’ grants, and generic system grants are invisible. Mixed result type.
Both AppListGrantsOptions and AgentListGrantsOptions accept the same filters — all AND-ed, each only narrowing the result:
| Option | Type | Default | Description |
|---|---|---|---|
providerId | string | — | Filter to one provider. OAuth: the provider ID. Managed secret: the per-secret slug, unique per app. |
status | string | — | Filter by grant status (e.g. "active", "expired", "revoked"). |
account | string | — | Filter by accountIdentifier (multi-account disambiguation). |
label | string | — | Filter to one sibling-grant label (multi-grant model; applies to both OAuth and managed-secret labels). |
endUserToken | string | — | Scope to one end user by their JWT — their direct grants plus live group-member grants. On App, takes precedence over userTokenGetter; on Agent, narrows the delegated OAuth grants (agent-owned managed-secret grants are unaffected). An invalid token is a hard 401, never a silent fall-through to app scope. |
limit | number | 100 | Page size (1..1000). |
offset | number | 0 | Page offset. |
Returns UnifiedGrantListResult. Each entry is either an OAuthGrantItem or a ManagedSecretGrantItem; branch on grantKind.
const page = await agent.listGrants({ limit: 50 });for (const grant of page.grants) { if (grant.grantKind === "oauth") { console.log(grant.providerId, grant.scopes); } else { console.log(grant.managedSecretSlug, grant.label); }}if (page.hasMore) { // Fetch next page with offset = page.offset + page.limit.}Throws AlterValueError for out-of-range limit / offset / providerId.
revokeGrant
Section titled “revokeGrant”Revoke a grant. App-only.
async revokeGrant( grantId: string, options?: RevokeGrantOptions,): Promise<RevokeGrantResult>| Option | Type | Description |
|---|---|---|
reason | string | Free-form reason stored on the audit row. |
Returns RevokeGrantResult carrying the grant ID, success flag, and revocation timestamp.
await app.revokeGrant(grantId, { reason: "User requested account deletion" });Revoking an OAuth grant also cascades the revocation to every agent delegation on it.
mintGrant
Section titled “mintGrant”Mint a sibling grant on an existing grant’s credential without starting a new authorization flow. App-only.
async mintGrant( sourceGrantId: string, options: { label: string; grantPolicy?: Record<string, unknown>; grantTags?: string[]; },): Promise<GrantInfo>| Parameter | Type | Description |
|---|---|---|
sourceGrantId | string | Active grant owned by the application. Its credential and principal binding are reused. |
label | string | Required sibling address. It must be unique among active grants on the credential. |
grantPolicy | Record<string, unknown> | Optional per-grant policy. This nested object uses the backend policy grammar’s snake_case keys. |
grantTags | string[] | Optional non-empty tag strings. |
The new grant copies the source principal binding but not its delegations. It can only tighten access through its own policy.
const sibling = await app.mintGrant(sourceGrantId, { label: "read-only", grantPolicy: { expires_at: "2026-08-01T00:00:00Z", restrictions: { allowed_methods: ["GET"] }, }, grantTags: ["reporting"],});console.log(sibling.grantId, sibling.credentialId, sibling.label);Returns GrantInfo. Throws AlterValueError for malformed local input, GrantNotFoundError when the source grant is unavailable, SiblingLabelConflictError when the label is already active, PolicyViolationError for an agent caller, and BackendError for invalid policy or grant-limit responses.
revokeDelegation
Section titled “revokeDelegation”Revoke an agent’s delegation on a grant. The grant itself stays active; only the named agent loses access.
// App: operator revokes a named agent's delegation.async revokeDelegation(grantId: string, agentId: string): Promise<void>
// Agent: agent revokes its own delegation on a grant.async revokeDelegation(grantId: string): Promise<void>Both paths are idempotent.
// Operator path.await app.revokeDelegation(grantId, agentId);
// Self-revoke path — the agent removes its own access.await agent.revokeDelegation(grantId);Throws AlterValueError when agentId is missing on the App path.
delegate
Section titled “delegate”Agent only. Onward (agent → agent) delegation — the second hop. An agent that already holds a grant mints a child of it for another agent, without the credential owner consenting again.
async delegate( grantId: string, toAgentId: string, options?: { delegable?: boolean; scopeConstraint?: string[] | null; ttlSeconds?: number | null; },): Promise<DelegationResult>| Parameter | Type | Description |
|---|---|---|
grantId | string | A grant the calling agent already holds. |
toAgentId | string | The agent that should receive the child grant. |
delegable | boolean | Whether the recipient may re-delegate the child onward. Opt-in per hop. Default false. |
scopeConstraint | string[] | null | Narrow the child to a subset of the parent’s provider scopes. Provide a non-empty array of at most 100 concrete scope strings, each ≤ 512 characters and a valid RFC 6749 scope-token (printable ASCII — no spaces, " or \; one atom per scope); "*", empty strings, and an empty array are rejected (a wildcard never narrows). Omit or pass null for full scope. A scope-narrowed grant is proxy-only — call it with proxyRequest(). |
ttlSeconds | number | null | Positive-integer lifetime cap in seconds. Omit or pass null for no child-specific cap. A value never extends past the parent; the backend clamps the child’s expiry to the parent’s. |
The held grant must have been created as delegable (chosen at connect time, within the developer’s delegation policy). A child can only narrow — fewer scopes, a shorter lifetime — never widen.
Returns DelegationResult — the freshly minted child grant (grantId, parentGrantId, depth, delegable, status, expiresAt).
Throws:
GrantNotDelegableError— the held grant is not delegable, or a managed secret’s operator ceiling blocks re-delegable grants (HTTP 403grant_not_delegable).GrantNotFoundError— the caller does not holdgrantId.AlterValueError— empty inputs, an invalid scope constraint, or a non-positive/non-integerttlSeconds.
import { Agent, GrantNotDelegableError } from "@alter-ai/alter-sdk";
const agent = new Agent({ apiKey: "alter_ak_…" });
try { const child = await agent.delegate("…", "…", { scopeConstraint: ["resource:write"], ttlSeconds: 3600, }); console.log(child.grantId, child.parentGrantId, child.depth);} catch (e) { if (e instanceof GrantNotDelegableError) { // The held grant cannot be passed on — request a delegable grant instead. } else { throw e; }} finally { await agent.close();}listGrants() returns each grant with parentGrantId (the grant it was minted under, null for a root) and depth (its distance from the root), so the full delegation chain reconstructs from the flat list.
createManagedSecretGrant
Section titled “createManagedSecretGrant”Provision a managed-secret grant. App-only.
async createManagedSecretGrant( managedSecretId: string, options: { principal: Principal; grantPolicy?: GrantPolicyInput; },): Promise<CreateGrantResult>The principal field is a discriminated union — TypeScript narrows it based on the type discriminator. Each principal kind binds the grant to a different identity surface; the label lives on the principal itself (required for user/group bindings, optional otherwise):
| Principal kind | type | Required fields | When to use |
|---|---|---|---|
UserPrincipal | "user" | userToken, label | One end user (resolved from JWT). |
GroupPrincipal | "group" | externalGroupId, idpId, label | All members of an IDP group inherit access. Requires an identity provider that supports group grants, with its webhook integration enabled — otherwise the backend rejects the call with a 422 group_principal_unsupported_idp error. |
SystemPrincipal | "system" | label (optional) | Server-to-server. No caller identity. |
AgentPrincipal | "agent" | — | Not accepted by this SDK method. Create agent-bound managed-secret grants via the Developer Portal flow instead. |
The optional grantPolicy overrides the TTL bounds inherited from the parent managed secret:
| Field | Type | Description |
|---|---|---|
expiresAt | string | null | Absolute ISO 8601 expiry. |
maxTtlSeconds | number | null | Cap on requested credential TTL. |
defaultTtlSeconds | number | null | Default TTL when callers don’t request one. |
import type { Principal } from "@alter-ai/alter-sdk";
const principal: Principal = { type: "user", userToken: callingUserJwt, label: "production-read-only",};
const grant = await app.createManagedSecretGrant("ms_…", { principal, grantPolicy: { maxTtlSeconds: 3600 },});console.log(grant.grantId, grant.principalType);Returns CreateGrantResult with the new grantId, the resolved principal type, and the label.
getApprovalStatus
Section titled “getApprovalStatus”Single-shot poll for an approval row.
async getApprovalStatus(approvalId: string): Promise<ApprovalStatus>Returns ApprovalStatus. The status field is one of:
"pending"— no approver decision yet."approved"— approver said yes; backend is about to execute."executing"— backend is calling the provider."executed"— finished; the result blob is available."denied"— approver said no."expired"— approval window elapsed without a decision."failed"— backend errored while executing the proxied call.
The returned ApprovalStatus exposes a derived isTerminal boolean — true when status is "denied", "expired", "executed", or "failed". Use result.isTerminal to check whether further polling is pointless.
Throws BackendError, NetworkError, TimeoutError.
awaitApproval
Section titled “awaitApproval”Poll until the approval reaches a terminal state, then return the result. Transient 502 / 503 / 504 responses and transport blips are retried within the deadline; permanent failures propagate immediately.
async awaitApproval( approvalId: string, options?: { timeoutMs?: number; pollIntervalMs?: number },): Promise<ApprovalResult>| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 300_000 (5 min) | Maximum local wait. Milliseconds. |
pollIntervalMs | number | 2_000 | Time between polls. Milliseconds. |
Returns ApprovalResult when the row reaches executed.
Throws:
ApprovalDeniedError— approver denied.ApprovalExpiredError— approval window elapsed.ApprovalExecutionFailedError— backend failed to execute after approval was granted.ApprovalTimeoutError— local wait elapsed before any decision was recorded. When the wait elapsed because the backend was returning transient errors, the message names the underlying cause and the original exception is preserved on.cause.
try { const final = await app.awaitApproval(approvalId, { timeoutMs: 10 * 60_000 }); return final.bodyJson();} catch (error) { if (error instanceof ApprovalDeniedError) { // Surface "request was rejected" to the user. } throw error;}Reading the response body
Section titled “Reading the response body”ApprovalResult carries the provider response as base64.
final.bodyBytes(); // Uint8Arrayfinal.bodyText(); // string (utf-8 default)final.bodyText("latin1");final.bodyJson(); // parsed JSONfinal.bodyTruncated is true if the backend truncated the body (very large responses).
Surfacing PendingApproval to the user
Section titled “Surfacing PendingApproval to the user”PendingApproval .approvalUrl is the deep link to the approver’s wallet UI. Surface it through whatever channel the application uses — modal, redirect, chat message, or push notification. Email is sent automatically when the backend has an email provider configured.
if (result instanceof PendingApproval) { await notifyApprover({ title: "Approval requested", expiresAt: result.expiresAt, url: result.approvalUrl, }); await app.awaitApproval(result.approvalId, { timeoutMs: 60 * 60_000 });}