Skip to content

TypeScript SDK

Calling APIs

Credential-injected provider calls — request(), proxyRequest(), AWS SigV4, scopes catalog.

Every credential-injection path lives on this page. The SDK fetches the token, injects it into the outbound request, calls the provider, logs the call for audit, and returns the response. The token is never returned to application code.

// Direct provider call, SDK process executes
await app.request(method, url, options);
// Backend-mediated execution (HITL approval, server-side isolation)
await app.proxyRequest({ method, url, grantId, ... });
// AWS SigV4 — call AWS endpoints via request()
await app.request("GET", "https://sts.us-east-1.amazonaws.com/...", { grantId });
// Scope catalog discovery
await app.scopes.list();
PatternToken leaves backend?HITL approval?When to use
request()Yes — SDK process holds it brieflyNo (rejects HITL grants with hitl_grant_requires_proxy)Default. Direct provider call, lowest latency. AWS SigV4 is computed transparently.
proxyRequest()No — backend executesYesHITL grants, MCP/serverless without long-lived connections, compliance requires zero-egress.

For scopes.list() see the catalog section below. For the AWS SigV4 path on request() see AWS.

async request(
method: HttpMethod | string,
url: string,
options?: RequestOptions,
): Promise<AlterResponse>

Retrieve a credential from the backend, inject it into the outgoing HTTP request, and call the provider. The token is never returned to application code.

Available on both App and Agent.

import { App, HttpMethod } from "@alter-ai/alter-sdk";
const app = new App({
apiKey: process.env.ALTER_API_KEY!,
userTokenGetter: () => getCurrentUserJwt(),
});
const response = await app.request(
HttpMethod.GET,
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
{
provider: "google",
queryParams: { maxResults: "10" },
reason: "Render upcoming events",
},
);
const events = await response.json();

The SDK supports two ways to pick which credential to inject. Exactly one must be specified.

Resolve the credential by identity + provider. The backend looks up the active grant for the calling user (resolved from the JWT) or the calling agent (resolved from its delegation rows) on the named provider.

await app.request("GET", url, {
provider: "google",
userToken: callingUserJwt,
});

For App, identity comes from userTokenGetter on the constructor or the per-call userToken option. For Agent, identity comes from the agent’s API key — userToken disambiguates between multiple delegations on the same provider.

Use an explicit grant ID. Bypasses identity resolution.

await app.request("GET", url, { grantId: "grnt_…" });

Use direct mode when an upstream layer has already resolved which grant to use, when the grant has no associated user (system grants, agent-owned managed secrets), or when the caller wants to scope an audit row to a specific grant.

ParameterTypeDescription
methodHttpMethod | stringHTTP method. Case-insensitive.
urlstringProvider URL. Must start with https:// or http://. Path templates use {name} placeholders resolved via pathParams.
optionsRequestOptionsSee below.
OptionTypeDescription
grantIdstringExplicit grant ID for direct resolution. Mutually exclusive with provider.
providerstringIdentity-resolution key. OAuth: the provider id (e.g. "google"). Managed secret: the per-secret slug (e.g. "stripe-production"), unique per app. Mutually exclusive with grantId.
accountstringAccount disambiguator. Only valid alongside provider.
labelstringSibling-grant disambiguator — selects one of several same-provider grants (the resolution key is provider + label). Only valid alongside provider.
userTokenstringPer-call user JWT. Overrides userTokenGetter for this call.
appUserIdstringPer-call AppUser UUID. Equivalent in authority to userToken but skips JWT validation when the caller has already resolved the user upstream.
callerstringPer-call caller override for audit attribution.
jsonRecord<string, unknown>JSON request body. The SDK serializes and sets Content-Type: application/json. Mutually exclusive with body.
bodyUint8Array | ArrayBuffer | stringRaw request body. The caller is responsible for Content-Type on binary payloads. Mutually exclusive with json.
extraHeadersRecord<string, string>Additional request headers. The credential-injection header is overwritten if present (the SDK logs a warning).
queryParamsRecord<string, string | number | boolean>Query-string parameters. Values are coerced to strings.
pathParamsRecord<string, string>Substitutions for {name} placeholders in url. Each value is URL-encoded.
reasonstringFree-form reason for the call. Stored on the audit row.
contextRecord<string, string>Application metadata attached to the audit row (e.g. runId, toolName).

AlterResponse — a standard Response with one extra property:

  • retryInfo (RetryInfo \| null) — populated when the backend had to retry the token refresh. null when the token was served from cache or refreshed on the first attempt.

The body is consumed exactly once. Use await response.json(), await response.text(), or response.body as you would with fetch.

const response = await app.request(
HttpMethod.POST,
"https://api.github.com/repos/{owner}/{repo}/issues",
{
provider: "github",
userToken: callingUserJwt,
pathParams: { owner: "octocat", repo: "hello-world" },
json: { title: "Bug report", body: "Steps to reproduce…" },
reason: "User-submitted bug report",
},
);

Identity mode for a managed secret (by slug)

Section titled “Identity mode for a managed secret (by slug)”
// `provider` is the managed secret's slug (e.g. "stripe-production"), not "stripe".
const response = await app.request(HttpMethod.POST, "https://api.stripe.com/v1/charges", {
provider: "stripe-production",
userToken: callingUserJwt,
json: { amount: 1000, currency: "usd" },
});
await app.request(
HttpMethod.GET,
"https://api.stripe.com/v1/customers",
{
grantId,
queryParams: { limit: 100 },
reason: "Nightly reconciliation",
context: { runId, jobId, batch: "customers" },
},
);
const response = await app.request("GET", url, { grantId });
if (response.retryInfo !== null) {
console.log(
`Token refresh succeeded on attempt ${response.retryInfo.successfulAttempt}`,
);
}

async proxyRequest(args: {
method: HttpMethod | string;
url: string;
grantId?: string;
provider?: string;
account?: string;
label?: string;
jsonBody?: unknown;
headers?: Record<string, string>;
queryParams?: Record<string, unknown>;
reason?: string;
}): Promise<PendingApproval | ApprovalResult>

Send the full request to the Alter backend, which retrieves the credential, calls the provider, and returns the response. The token never reaches the SDK process.

Available on both App and Agent.

proxyRequest() takes a single options object — there are no positional method / url arguments.

const result = await app.proxyRequest({
method: "POST",
url: "https://api.example.com/transfer",
grantId,
jsonBody: { amount: 1000, currency: "usd" },
reason: "Quarterly payout",
});
  • HITL grants. Grants with requires_approval configured can only be exercised through proxyRequest()request() rejects them with hitl_grant_requires_proxy.
  • Server-side credential isolation. The token never crosses the SDK boundary. The backend is the only system that observes the plaintext.
  • Wire-level audit. The backend records the full request/response on the audit row, including bytes and headers.
  • Tight policy enforcement. The backend re-evaluates policy at execution time.

For everything else, request() avoids the extra hop.

  • The provider response body is buffered by the backend, base64-encoded, and may be truncated when very large. Proxy mode is not intended for downloads, Server-Sent Events, WebSockets, or long-lived streaming responses.
  • Auth and cookie-related provider response headers are stripped before the SDK receives the response. Flows that depend on Set-Cookie, WWW-Authenticate, or returned authorization headers may need request() or the provider client directly.
  • Managed-secret proxy calls require destination hosts to be configured on the secret. If the allowlist is missing or does not match the requested host, the call is blocked before the credential is injected.
  • proxyRequest() does not automatically retry the initial call. Add application-level retry/backoff for network failures and timeouts. For HITL grants, retry carefully because submitting the same call again can create another pending approval.
  • The CLI passthrough command uses retrieve mode, not proxy mode. It is useful for testing request(), but it does not reproduce proxy-only behavior such as HITL execution, destination-host allowlist enforcement, or response truncation.
FieldTypeDescription
methodHttpMethod | stringHTTP method. Case-insensitive.
urlstringProvider URL. Must be a non-empty string.
grantIdstringDirect-mode grant ID. Exactly one of grantId or provider must be supplied — proxyRequest() shares the same resolution contract as request(): one match resolves, zero is a not-found error, several throw AmbiguousGrantError.
providerstringIdentity-mode key (e.g. "google", or a managed secret’s per-secret slug). Mutually exclusive with grantId.
accountstringAccount disambiguator for provider resolution.
labelstringSibling-grant disambiguator for provider resolution (the resolution key is provider + label). Only valid alongside provider.
jsonBodyunknownJSON-serializable body. Accepts plain objects, arrays, strings, numbers, booleans, and null. Class instances, Date, Map, and Set are rejected at the SDK boundary (they serialize to {} via JSON.stringify).
headersRecord<string, string>Additional headers. Authorization-bearing headers (Authorization, Cookie, x-api-key, x-amz-security-token) are rejected — the backend injects credentials at execution time.
queryParamsRecord<string, unknown>Query-string parameters.
reasonstringFree-form reason. Stored on the audit row and shown to the approver when HITL is configured.

The return type is a discriminated union. Use instanceof or the status field to branch.

  • ApprovalResult — the call ran synchronously and the provider response is on this object. Typical when the grant has no HITL requirement, or when the approval was pre-granted.
  • PendingApproval — the backend created an approval row and is waiting for an approver. The call has NOT executed yet. Pass approvalId to awaitApproval() or poll with getApprovalStatus().
import { PendingApproval } from "@alter-ai/alter-sdk";
const result = await app.proxyRequest({
method: "POST",
url: "https://api.example.com/transfer",
grantId,
jsonBody: { amount: 1000 },
reason: "Quarterly payout",
});
if (result instanceof PendingApproval) {
// Surface result.approvalUrl to the approver, then poll.
const final = await app.awaitApproval(result.approvalId, {
timeoutMs: 5 * 60_000,
});
console.log(final.statusCode, final.bodyText());
} else {
// Synchronous response.
console.log(result.statusCode, result.bodyJson());
}

The status field is "pending" on PendingApproval. ApprovalResult has no status field.

  • AlterValueError — neither grantId nor provider was supplied (or both were), an identifier value is an empty string, account/label was supplied without provider, url is missing, jsonBody has an unsupported shape, or an authorization-bearing header was supplied.
  • AlterSDKError — the SDK has been closed.
  • ApprovalDeniedError — backend rejected the call (policy denial that does not require human review).
  • ApprovalExpiredError — the approval window elapsed before a decision was recorded.
  • ApprovalExecutionFailedError — backend failed to execute the proxied call after the approval was granted.
  • BackendError — other backend failure.
  • NetworkError, TimeoutError — transport failures to the Alter backend.

ApprovalResult carries the provider response body as base64. Three helpers decode on demand:

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

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


The TypeScript SDK signs AWS requests using SigV4 automatically. There is no separate AWS client class — call AWS exactly like any other provider with request() and pass the URL of the AWS service endpoint. The SDK recognizes grants that resolve to AWS credentials and computes the multi-header signature transparently.

import { App, HttpMethod } from "@alter-ai/alter-sdk";
const app = new App({ apiKey: process.env.ALTER_API_KEY! });
const response = await app.request(
HttpMethod.GET,
"https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15",
{ grantId },
);
const xml = await response.text();

When the grant resolves to an AWS credential, the SDK signs the outgoing request with SigV4 (Authorization, x-amz-date, x-amz-content-sha256, and x-amz-security-token when applicable) before sending. The credential is never returned to application code.

The signing region and service are part of the credential. The SDK uses them verbatim, so if the URL targets a different region than the credential is configured for, AWS rejects the call with SignatureDoesNotMatch. Provision a credential per region (or update the existing one) to match the target endpoint.

For the simple “call a URL” shape, app.request() is sufficient — the SDK computes the signature for you:

const response = await app.request(
HttpMethod.GET,
"https://my-bucket.s3.us-east-1.amazonaws.com/path/to/object",
{ grantId },
);

For everything outside that shape — multipart uploads, streaming downloads, paginators — use the official AWS SDK clients (@aws-sdk/client-s3, etc.) and route each call through proxyRequest() so signing stays inside Alter.

Same exception set as request(). Two AWS-specific failure modes:

  • BackendError — AWS signing material is incomplete for this grant. Re-store the AWS credential with both the access key ID and secret access key.
  • ProviderAPIError with AWS SignatureDoesNotMatch — region or service slug on the credential does not match the URL.

The scopes namespace exposes the active scope catalog — the set of resource verbs and reserved actions the backend recognizes. Use it to render scope pickers, validate keys.derive({ scopes }) inputs, or sanity-check that a planned attenuation matches the deployment.

Available on both App and Agent.

async list(): Promise<ScopeCatalog>

No scope is required to read the catalog (it is static metadata), but the call still authenticates with the API key, passes through env / CIDR / rate-limit gates, and is rate-limited per key.

Returns ScopeCatalog.

FieldTypeDescription
scopeVersionnumberServer-advertised catalog version. Changes only on backend release.
resourcesRecord<string, ResourceScopes>Per-resource verb sets.
actionVerbsreadonly string[]Reserved action verbs (e.g. derive, admin).
deprecatedreadonly string[]Scopes still accepted but flagged for removal in a future release.

The catalog is not cached by the SDK — call list() on demand. The shape is small and the call is light.

import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });
const catalog = await app.scopes.list();
console.log(`Scope version: ${catalog.scopeVersion}`);
for (const [resource, { verbs }] of Object.entries(catalog.resources)) {
console.log(`${resource}: ${verbs.join(", ")}`);
}
const validScopes = new Set(
Object.entries(catalog.resources).flatMap(([resource, { verbs }]) =>
verbs.map((verb) => `${resource}:${verb}`),
),
);
const requested = ["tokens:retrieve", "grants:read"];
const unknown = requested.filter((scope) => !validScopes.has(scope));
if (unknown.length > 0) {
throw new Error(`Unknown scopes: ${unknown.join(", ")}`);
}

Throws BackendError, AlterValueError (on a wire-shape regression), NetworkError, TimeoutError.

When the application runs an OpenTelemetry SDK with an active span, every call the SDK makes to Alter automatically carries the standard W3C traceparent header — request(), proxyRequest(), approval polling, and the audit-reporting calls included. Alter uses it as the trace context for that request’s audit events, and any spans the organization streams to its own OTLP collector join the application’s traces instead of starting disconnected ones.

No configuration is required: @opentelemetry/api is an optional peer dependency that the SDK never installs or requires. When the application has it — including under pnpm’s isolated mode and Yarn PnP — the SDK uses it; without it (or without an active span) the header is simply omitted and nothing changes. The integration is best-effort by design: it can never fail a vault call (the very first call pays a one-time, in-process lookup of the optional module).

import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("the-application");
// `app` from the quick start.
await tracer.startActiveSpan("handle-user-request", async (span) => {
try {
// This call's audit events share the surrounding trace's ids.
const response = await app.request(HttpMethod.GET, url, { grantId });
console.log(response.status);
} finally {
span.end();
}
});

Only trace/span identifiers and a sampling flag travel in the header — no payloads, no user identifiers.