Skip to content

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 catalog
const catalog = await app.oauthProviders.list();
// Connect flows
const 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 convenience
const authSession = await app.createAuthSession(); // headless / remote user
const result = await app.pollAuthSession(authSession.sessionToken);
// Grants
const 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" });
// Delegations
await app.revokeDelegation(grantId, agentId); // operator
await agent.revokeDelegation(grantId); // agent self-revoke
// Approvals
const final = await app.awaitApproval(approvalId, { timeoutMs: 600_000 });
const status = await app.getApprovalStatus(approvalId);

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>
OptionTypeDefaultDescription
forceRefreshbooleanfalseBypass 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>");

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>
OptionTypeDescription
allowedProvidersstring[]Restrict to specific provider IDs (for example, ["<provider-id>"]). When omitted the user can pick any configured provider.
returnUrlstringURL the browser redirects to after consent (mobile / single-page flows).
allowedOriginstringOrigin allowed for the postMessage completion event (popup flow).
metadata{ ipAddress?: string; userAgent?: string }Forwarded to the audit row.
grantPolicyGrantPolicyInputOptional absolute expiry and TTL bounds the user picks from on the consent screen. TTL fields are seconds.
requiredScopesRecord<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).
agentstringAgent UUID or managed-agent name. When set, on approval the SDK returns a delegation grant that the agent uses as grantId on request().
requestedGrantRequestedGrantSibling-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.
delegablebooleanOnly 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.
scopeConstraintstring[]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.
userTokenstringPer-call user JWT. Overrides userTokenGetter; omit both for an app-scoped/headless session.
switchAccountbooleanForce 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.
allowUserPolicyRulesbooleanWhether 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 ConnectSessionconnectUrl 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);

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>
OptionTypeDescription
templateSlugstringCanonical managed-secret template slug in kebab-case. Required.
delegatedAgentIdstringUUID of the agent being authorized. Required.
userTokenstringIDP JWT identifying the consenting user. Required.
requestedTtlSecondsnumberOptional 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.
delegatedAgentNamestringDisplay name shown on the consent screen. Defaults to the agent’s stored display name.
allowedOriginstringOrigin allowed for the postMessage event.
returnUrlstringURL for the mobile redirect flow.
allowUserPolicyRulesbooleanWhether 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.

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[]>
OptionTypeDefaultDescription
timeoutMsnumber300_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.
pollIntervalMsnumber2_000Positive, 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_grants contains the typed failures in snake_case wire form.
  • BackendError — malformed terminal error fields or another malformed backend response.

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).

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[]>
OptionTypeDefaultDescription
providersstring[]Restrict to specific providers.
timeoutnumber300_000Positive, finite maximum wait. Milliseconds. An explicit value is honored; it does not shorten the server session. Elapsed time uses a monotonic clock.
pollIntervalnumber2_000Positive, finite time between polls. Milliseconds.
openBrowserbooleantrueWhen false, prints the URL instead of launching the browser.
grantPolicyGrantPolicyInputOptional absolute expiry and TTL bounds passed to the Connect UI. TTL fields are seconds.
allowUserPolicyRulesbooleantrueWhether 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:

  • AlterValueErrorgrantPolicy is malformed or allowUserPolicyRules is not a boolean.
  • ConnectTimeoutError — Alter received no completion callback before the local deadline or observed session expiry; inspect details.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 carries details.failed_grants; partial failures are returned as ConnectResult.failedGrants.

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>
OptionTypeDefaultDescription
timeoutMsnumber300_000Maximum wait. Milliseconds.

Returns AuthResult with userToken and userInfo.

App-only method.

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 };

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>
OptionTypeDefaultDescription
timeoutMsnumber300_000Maximum wait. Milliseconds. The Python SDK’s equivalent takes seconds.
pollIntervalMsnumber2_000Milliseconds 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 });

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 userTokenGetter configured (or endUserToken passed), 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 (accessVia marks ownership vs 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:

OptionTypeDefaultDescription
providerIdstringFilter to one provider. OAuth: the provider ID. Managed secret: the per-secret slug, unique per app.
statusstringFilter by grant status (e.g. "active", "expired", "revoked").
accountstringFilter by accountIdentifier (multi-account disambiguation).
labelstringFilter to one sibling-grant label (multi-grant model; applies to both OAuth and managed-secret labels).
endUserTokenstringScope 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.
limitnumber100Page size (1..1000).
offsetnumber0Page 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.

Revoke a grant. App-only.

async revokeGrant(
grantId: string,
options?: RevokeGrantOptions,
): Promise<RevokeGrantResult>
OptionTypeDescription
reasonstringFree-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.

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>
ParameterTypeDescription
sourceGrantIdstringActive grant owned by the application. Its credential and principal binding are reused.
labelstringRequired sibling address. It must be unique among active grants on the credential.
grantPolicyRecord<string, unknown>Optional per-grant policy. This nested object uses the backend policy grammar’s snake_case keys.
grantTagsstring[]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.

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.

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>
ParameterTypeDescription
grantIdstringA grant the calling agent already holds.
toAgentIdstringThe agent that should receive the child grant.
delegablebooleanWhether the recipient may re-delegate the child onward. Opt-in per hop. Default false.
scopeConstraintstring[] | nullNarrow 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().
ttlSecondsnumber | nullPositive-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 403 grant_not_delegable).
  • GrantNotFoundError — the caller does not hold grantId.
  • AlterValueError — empty inputs, an invalid scope constraint, or a non-positive/non-integer ttlSeconds.
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.

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 kindtypeRequired fieldsWhen to use
UserPrincipal"user"userToken, labelOne end user (resolved from JWT).
GroupPrincipal"group"externalGroupId, idpId, labelAll 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:

FieldTypeDescription
expiresAtstring | nullAbsolute ISO 8601 expiry.
maxTtlSecondsnumber | nullCap on requested credential TTL.
defaultTtlSecondsnumber | nullDefault 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.


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.

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>
OptionTypeDefaultDescription
timeoutMsnumber300_000 (5 min)Maximum local wait. Milliseconds.
pollIntervalMsnumber2_000Time between polls. Milliseconds.

Returns ApprovalResult when the row reaches executed.

Throws:

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;
}

ApprovalResult carries the provider response as base64.

final.bodyBytes(); // Uint8Array
final.bodyText(); // string (utf-8 default)
final.bodyText("latin1");
final.bodyJson(); // parsed JSON

final.bodyTruncated is true if the backend truncated the body (very large responses).

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 });
}

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.