Reference
Non-proxy requests (discouraged)
Advanced credential-release exception. Disabled by default and requires administrator risk acknowledgement.
Non-proxy use is available for any provider. It is refused by default and allowed only by a scoped, expiring exception that an organization administrator sets in policy. Use it where a proxy cannot be used, for example AWS access from a private VPC. Python boto3_client(), request() and alter sdk-passthrough request all release the raw credential this way.
Enable only after accepting the risk
Section titled “Enable only after accepting the risk”An organization administrator must open Settings → API key policy → Credential-release exceptions and choose Add credential-release exception…. Name a provider or a specific grant/managed secret, and the one app or agent allowed to receive it. App exceptions never cover agents. Supply a reason and expiry, then accept the warning for that credential type. There is no “all” option and no operation or endpoint scope: once released, Alter cannot limit what the credential does. Only a signed-in organization administrator can create, revoke or manage exceptions; PATs, SDKs and the CLI cannot write them.
| Credential type | Default exception lifetime | Maximum | Warning |
|---|---|---|---|
| Static API keys and managed credentials | 1 hour | 90 days | A released key may remain usable indefinitely; rotate or revoke it at the provider. Managed credentials use this conservative budget even when they mint tokens. |
| OAuth access tokens | 24 hours | 90 days | A released access token remains usable until its own expiry or provider revocation. |
These limits control future releases, not the lifetime of credentials already released. Expiry forces a review; it does not shorten the exposure of a key that outlives it. Choose Renew exception before an exception ends to keep its identity and extend it; renewing accepts the current warning again. The portal marks an exception Expiring soon in the last fifth of its life. Revoking is immediate. The exception list starts empty for every organization. Each entry displays its creator, reason, expiry and accepted warning version, and an entry whose warning has been superseded shows Needs re-consent and allows no release until renewed. Expiry is enforced on every request; no cleanup job is needed to stop an expired allowance. The audit record of each release names the exception that allowed it.
An exception naming an agent also applies to that application’s API keys when they assert the agent as their caller, because an application key can act as its agents. The audit record names both the key and the asserted caller.
Disable all credential release immediately blocks new releases, including requests still retrieving a credential. Resume unexpired exceptions only restores existing, unexpired exceptions. Revoke an individual entry to remove its authority. These actions and consent are audited. Rotate or revoke already-released credentials at the provider if necessary.
Grant restrictions stay in force: an exception cannot bypass HITL, method/endpoint limits, user limits, delegation restrictions or policy denial. Runtime keys still need tokens:retrieve (and audit:emit when using request()); add those advanced scopes only for a scoped, approved use case.
Migration and release notice
Section titled “Migration and release notice”Existing non-proxy callers are refused as soon as this release deploys. They receive NonProxyDisabledError (non_proxy_disabled) unless an unexpired exception covers that exact caller and that provider or credential, and the disable-all switch is off. To recover after the deploy, an organization administrator opens Settings → API key policy → Credential-release exceptions, chooses Add credential-release exception…, and names the provider or credential and the caller. It takes effect on the next request. Older SDKs raise PolicyViolationError with policy_error / policyError set to non_proxy_disabled; no policy rule is involved. Or update callers to Python proxy_request() or TypeScript proxyRequest() and use proxy:execute. Proxy results use status_code / statusCode and body_json() / bodyJson(), and may first return PendingApproval. Handle that result with await_approval() / awaitApproval().
A proxied call returns a provider 4xx or 5xx as a result, where request() raised. Check status_code / statusCode before reading the body.
Proxy execution covers the transfers that used to need a direct call. Binary bodies up to 5 MiB go through proxy_request() / proxyRequest(). Larger uploads go through proxy_upload_request() / proxyUploadRequest(), and downloads and Server-Sent Events through proxy_stream_request() / proxyStreamRequest(). WebSocket upgrades are not available through the proxy. The Python boto3_client() bridge and alter sdk-passthrough request use non-proxy execution and require this exception. Both stay available. Neither is routed through the proxy. Without an exception, boto3_client() calls raise NonProxyDisabledError, and alter sdk-passthrough request prints non_proxy_disabled with the recovery steps and exits 1. To recover, an organization administrator creates an exception naming the caller and the provider or credential, or the caller moves to proxy_request() / proxyRequest(). The CLI command’s output and exit codes are otherwise unchanged. SDK request() methods retain their existing return types; they never silently fall back to proxy execution.
Python advanced reference
Section titled “Python advanced reference”request() — Python
Section titled “request() — Python”The direct-execution entry point for calling a provider API with an Alter-managed credential. Use proxy_request() for backend-mediated execution.
async def request( method: HttpMethod | str, url: str, *, grant_id: str | None = None, provider: str | None = None, account: str | None = None, label: str | None = None, json: dict[str, Any] | list[Any] | str | bool | int | float | None = None, extra_headers: dict[str, str] | None = None, query_params: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, reason: str | None = None, context: dict[str, str] | None = None, body: bytes | None = None, caller: str | None = None, user_token: str | None = None, app_user_id: str | None = None,) -> httpx.Response| Parameter | Type | Default | Description |
|---|---|---|---|
method | HttpMethod | str | — | HTTP method. The HttpMethod enum is recommended for IDE autocomplete. |
url | str | — | Full provider URL. HTTPS is required; HTTP is accepted only for localhost, 127.0.0.1, or [::1] development endpoints. Supports {name} placeholders resolved via path_params. |
grant_id | str | None | None | Direct-mode grant identifier. Mutually exclusive with provider. |
provider | str | None | None | Identity-mode key. OAuth: a provider id (e.g. "provider-id"). Managed secrets: the provider’s slug (e.g. "service-production"), unique per app — not its template id. Requires user_token_getter on the client or user_token= per call. Mutually exclusive with grant_id. |
account | str | None | None | Disambiguator for identity mode when the resolved user has multiple grants on the same provider (see AmbiguousGrantError). |
label | str | None | None | Sibling-grant disambiguator for identity mode — selects one of several same-provider grants (the resolution key is provider + label). Only valid alongside provider. |
json | dict | list | str | bool | int | float | None | None | JSON-serialized request body. Mutually exclusive with body. Numbers must be finite; NaN and positive/negative infinity raise AlterValueError before any request. |
body | bytes | None | None | Raw bytes request body. Mutually exclusive with json. |
extra_headers | dict[str, str] | None | None | Extra request headers. Authorization is injected by the SDK and must not be set here. |
query_params | dict[str, Any] | None | None | Query string parameters. Ordinary requests replace the URL query; AWS SigV4 requests replace matching keys and preserve other keys. The SDK checks the URL length after selecting the credential format and applying credential query parameters. |
path_params | dict[str, str] | None | None | Values substituted into {name} placeholders in url. URL-encoded. |
reason | str | None | None | Audit reason recorded with the call. |
context | dict[str, str] | None | None | Audit-correlation context. Capped at 4096 bytes, 20 keys, 64-char keys, 512-char values. Alter writes its own audit facts under attribution_version, caller_attribution, cascade_count, causation, connect_session_id, consent, credential_release, delegation, delegation_count, delegation_ids, delegation_terms, delegations, delegations_truncated, requested_by, requested_grant_id, revoked_by_actor and subject_app_user_id, so the SDK refuses a context carrying any of them with AlterValueError (a copy sent by an older SDK is dropped). Falls back to the ambient context from Agent.trace() / @alter_tool / @alter.tool() when omitted. |
caller | str | None | None | Per-call override of the client’s caller identifier. |
user_token | str | None | None | Per-call user JWT for identity mode. Wins over user_token_getter. |
app_user_id | str | None | None | Per-call user identifier for agent-delegation disambiguation. |
To narrow scopes or attach a request rule to a call, mint a constrained sub-client with with_constraints() rather than passing per-call policy arguments.
Returns: httpx.Response. Inspect response.status_code, response.json(), response.text, etc.
Raises:
AlterSDKError— missing bothgrant_idandprovider, supplying both, supplying bothjsonandbody, client closed, URL with disallowed scheme.AlterValueError— badcontextshape, malformeduser_token/app_user_id,app_user_idorlabelsupplied withoutprovider, an unresolved{name}placeholder left inurlafterpath_params, or ajsonvalue that is not strict JSON (including non-finite numbers, circular references, or custom objects).GrantNotFoundError,GrantExpiredError,GrantRevokedError,GrantDeletedError,CredentialRevokedError— grant-state failures (see Errors).AmbiguousGrantError— identity-mode resolution matched multiple grants. Inspecte.account_identifiersand retry withaccount=.NoDelegatedGrantError— agent caller has no access path to the provider.NonProxyDisabledError— no live exception covers this caller and credential, or all release is disabled.detailscarriescaller_kind,caller_id,grant_id,provider_id,managed_secret_idandcredential_kind.RestrictedGrantRequiresProxyError— the grant is proxy-only:details["error"]isrestricted_grant_requires_proxy(method/endpoint restrictions),delegated_grant_requires_proxy(a delegation or scope-narrowed grant),hitl_grant_requires_proxy(approval required) oruser_limits_require_proxy(the grant’s user set a usage limit on it — a quota, a time window or an operation rule, for example from the Connect page or the wallet — on an OAuth and a managed-secret grant alike). Only a call the backend makes itself can enforce these; callproxy_request()instead.AgentNotFoundError— the configuredcalleridentifier (client-level or per-call) is a UUID naming no active managed agent in this app;details["error"] == "caller_agent_unresolved". Fix the asserted caller identity.PolicyViolationError,InsufficientScopeError,ScopeReauthRequiredError,TokenRefreshInProgressError— policy / scope / refresh failures.ProviderAPIError— non-2xx provider response (raised only when the SDK can map it to a typed error).NetworkError,TimeoutError— connectivity failures.
Provider transport failures still emit an API-call audit event before the typed error is raised; closing the root client ensures that pending work completes before exit.
Direct grant mode
Section titled “Direct grant mode”Pass grant_id= when the application has the grant identifier in hand (stored after a Connect flow, returned by list_grants(), etc.).
from alter_sdk import App, HttpMethod
app = App(api_key="alter_rk_…")
resp = await app.request( HttpMethod.GET, "https://api.provider.example/v1/resources", grant_id="11111111-2222-3333-4444-555555555555", query_params={"limit": 10},)resources = resp.json()Identity mode
Section titled “Identity mode”Pass provider= when the caller is acting on behalf of a known user. The SDK resolves the user’s JWT (from user_token_getter or user_token=) and looks up the grant.
from alter_sdk import App
def jwt_for_current_user() -> str: return request_state.user_jwt
app = App( api_key="alter_rk_…", user_token_getter=jwt_for_current_user,)
resp = await app.request( "GET", "https://api.provider.example/v1/resources", provider="provider-id",)from alter_sdk import App
app = App(api_key="alter_rk_…")
resp = await app.request( "GET", "https://api.provider.example/v1/resources", provider="provider-id", user_token=user_jwt,)from alter_sdk import App, HttpMethod
app = App(api_key="alter_key_app_…")
# `provider` is the provider's slug, not its template name.resp = await app.request( HttpMethod.POST, "https://api.provider.example/v1/actions", provider="service-production", user_token=user_jwt, json={"resource_id": "resource_123"},)Disambiguating with account
Section titled “Disambiguating with account”When identity mode resolves to multiple grants on the same provider (a personal and a work account, or sibling grants with different labels), the backend returns 409 and the SDK raises AmbiguousGrantError. Resolution never guesses, and neither should the application: do not take the first candidate. Decide this at design time. If one user can connect several accounts of a provider, the product needs an account choice that is stored on the user and passed on every call, as account= or as the chosen grant_id:
from alter_sdk import AmbiguousGrantError
def choose_grant(candidates): # Show each candidate's label / account_identifier / account_display_name # to the user, store the chosen grant_id on the user, and reuse it. ...
try: resp = await app.request("GET", url, provider="provider-id", user_token=user_jwt, account=saved_account)except AmbiguousGrantError as e: grant_id = choose_grant(e.candidates) # grant_id together with the user's JWT keeps the grant bound to that user. resp = await app.request("GET", url, grant_id=grant_id, user_token=user_jwt)If the product never lets a user hold two accounts for a provider, an AmbiguousGrantError is a data problem worth surfacing rather than an error to recover from silently.
URL templates with path_params
Section titled “URL templates with path_params”resp = await app.request( "GET", "https://api.provider.example/v1/projects/{project}/items/{item_id}", grant_id=grant_id, path_params={"project": "example", "item_id": "42"},)Values are URL-encoded before substitution.
Audit context
Section titled “Audit context”context is propagated to the audit row for every call:
resp = await app.request( "POST", "https://api.provider.example/v1/actions", grant_id=grant_id, json=payload, reason="user_initiated_chat", context={"thread_id": "thr_abc", "tool": "summarize"},)When called inside an Agent.trace() block or from inside a @alter_tool / @alter.tool() body, the ambient context is used automatically if context= is not passed.
boto3_client() — Python
Section titled “boto3_client() — Python”Python only. Returns a native boto3 service client whose every call is routed through Alter: credential retrieval, SigV4 signing, policy enforcement, and audit logging happen in the SDK. The application code does not handle AWS keys, but they are released into the SDK process, so this requires a credential-release exception. Use it when an application prefers boto3’s response parsing, pagination, and botocore.exceptions.ClientError over request(). Requires the aws extra:
pip install 'alter-sdk[aws]'import asyncioimport os
from alter_sdk import App
async def main(): async with App(api_key=os.environ["ALTER_API_KEY"]) as app: s3 = await app.boto3_client( "s3", grant_id=os.environ["AWS_GRANT_ID"], region_name="us-east-1", reason="Listing objects for the data pipeline", ) # boto3 methods are synchronous — run them in a thread from async code. resp = await asyncio.to_thread(s3.list_objects_v2, Bucket="my-bucket", MaxKeys=10) print([obj["Key"] for obj in resp.get("Contents", [])])
asyncio.run(main())| Parameter | Type | Default | Description |
|---|---|---|---|
service_name | str | — | AWS service name, e.g. "s3" or "dynamodb". |
grant_id | str | — | The AWS managed-secret grant to sign with. Keyword-only. |
region_name | str | None | "us-east-1" | AWS region for the client. |
timeout | float | None | client timeout + 5 s | Wall-clock budget per call, covering credential retrieval and the HTTP request. |
reason | str | None | None | Audit reason recorded on every request the client makes. |
context | dict[str, str] | None | None | Caller-supplied audit context, correlated on every request. |
Available on both App and Agent. Every call the client makes produces the same audit row a request() call does. One grant serves every AWS service; construct one client per service. See AWS for the credential setup and a multi-service example.
TypeScript advanced reference
Section titled “TypeScript advanced reference”request() — TypeScript
Section titled “request() — TypeScript”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(),});try { const response = await app.request( HttpMethod.GET, "https://api.example.com/v1/resources", { provider: "<provider-id>", queryParams: { limit: "10" }, reason: "Render recent resources", }, ); const resources = await response.json();} finally { await app.close();}Resolution modes
Section titled “Resolution modes”The SDK supports two ways to pick which credential to inject. Exactly one must be specified.
Identity mode — provider
Section titled “Identity mode — provider”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: "<provider-id>", 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.
Direct mode — grantId
Section titled “Direct mode — grantId”Use an explicit grant ID instead of a provider lookup. A per-call userToken, or the constructor’s userTokenGetter, is still forwarded so the backend can enforce user ownership of that grant.
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-secret grants), or when the caller wants to scope an audit row to a specific grant.
Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
method | HttpMethod | string | HTTP method. Case-insensitive. |
url | string | Provider URL. HTTPS is required; HTTP is accepted only for localhost, 127.0.0.1, or [::1] development endpoints. Path templates use {name} placeholders resolved via pathParams. |
options | RequestOptions | See below. |
RequestOptions
Section titled “RequestOptions”| Option | Type | Description |
|---|---|---|
grantId | string | Explicit grant ID for direct resolution. Mutually exclusive with provider. |
provider | string | Identity-resolution key. OAuth: the provider ID. Managed secrets: the provider’s slug, unique per app — not its template id. Mutually exclusive with grantId. |
account | string | Account disambiguator. Only valid alongside provider. |
label | string | Sibling-grant disambiguator — selects one of several same-provider grants (the resolution key is provider + label). Only valid alongside provider. |
userToken | string | Per-call user JWT. Overrides userTokenGetter for this call. |
appUserId | string | Per-call AppUser UUID. Equivalent in authority to userToken but skips JWT validation when the caller has already resolved the user upstream. |
caller | string | Per-call caller override for audit attribution. |
json | unknown | JSON-serializable request body: plain objects, arrays, strings, finite numbers, booleans, or null. The SDK serializes and sets Content-Type: application/json. Mutually exclusive with body. |
body | Uint8Array | ArrayBuffer | string | Raw request body. The caller is responsible for Content-Type on binary payloads. Mutually exclusive with json. |
extraHeaders | Record<string, string> | Additional request headers. The credential-injection header is overwritten if present (the SDK logs a warning). |
queryParams | Record<string, string | number | boolean> | Query-string parameters. Values are coerced to strings. Ordinary requests append these values; AWS SigV4 requests replace existing values with the same key. URL-length validation measures the selected request after credential injection. |
pathParams | Record<string, string> | Substitutions for {name} placeholders in url. Each value is URL-encoded. |
reason | string | Free-form reason for the call. Stored on the audit row. |
context | Record<string, string> | Application metadata attached to the audit row (e.g. runId, toolName). Maximum 20 entries; keys are limited to 64 characters, values to 512 characters, and the serialized object to 4096 characters. Alter writes its own audit facts under attribution_version, caller_attribution, cascade_count, causation, connect_session_id, consent, credential_release, delegation, delegation_count, delegation_ids, delegation_terms, delegations, delegations_truncated, requested_by, requested_grant_id, revoked_by_actor and subject_app_user_id, so the SDK refuses a context carrying any of them with AlterValueError (a copy sent by an older SDK is dropped). |
Returns
Section titled “Returns”AlterResponse — a standard Response with one extra property:
retryInfo(RetryInfo \| null) — populated when the backend had to retry the token refresh.nullwhen 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.
Throws
Section titled “Throws”AlterSDKError— bothgrantIdandproviderset, neither set, bothjsonandbodyset, or the SDK has been closed.AlterValueError— invalid/non-absolute URL or disallowed URL scheme; malformedjson,pathParams, orcontext;userTokenorappUserIdis an empty string;appUserIdorlabelis supplied withoutprovider; or an unresolved{name}placeholder remains inurl.GrantNotFoundError,GrantExpiredError,GrantRevokedError,GrantDeletedError,CredentialRevokedError— grant-state failures.ReAuthRequiredError— user JWT is invalid or expired.NonProxyDisabledError— no live exception covers this caller and credential, or all release is disabled.detailscarriescaller_kind,caller_id,grant_id,provider_id,managed_secret_idandcredential_kind.RestrictedGrantRequiresProxyError— the grant is proxy-only:details.errorisrestricted_grant_requires_proxy(method/endpoint restrictions),delegated_grant_requires_proxy(a delegation or scope-narrowed grant),hitl_grant_requires_proxy(approval required) oruser_limits_require_proxy(the grant’s user set a usage limit on it). CallproxyRequest()instead.AmbiguousGrantError— multiple grants match the identity + provider tuple. Passaccountor usegrantId.NoDelegatedGrantError— the calling agent has no delegation for the resolved user on this provider.AgentNotFoundError— the configuredcalleridentifier (client-level or per-call) is a UUID naming no active managed agent in this app;details.error === "caller_agent_unresolved". Fix the asserted caller identity.PolicyViolationError,InsufficientScopeError— backend authorization failures.ProviderAPIError— provider returned a 4xx/5xx that is not a scope failure.ScopeReauthRequiredError— provider returned 403insufficient_scope, or the backend already knew the grant’s scopes don’t cover the configured required set. Re-authorize the grant viacreateConnectSessionForError().NetworkError,TimeoutError— transport failures.
Provider transport failures still emit an API-call audit event before the typed error is raised; closing the root client ensures that pending work completes before exit.
Examples
Section titled “Examples”Identity mode with per-call JWT
Section titled “Identity mode with per-call JWT”const response = await app.request( HttpMethod.POST, "https://api.example.com/projects/{project}/items", { provider: "<provider-id>", userToken: callingUserJwt, pathParams: { project: "example-project" }, json: { title: "Example item", body: "Item details" }, reason: "User-submitted item", },);Disambiguating when several grants match
Section titled “Disambiguating when several grants match”Identity mode can match more than one grant for a user and provider (a personal and a work account, or sibling grants with different labels). The backend returns 409 and the SDK throws AmbiguousGrantError; resolution never guesses, and neither should the application. Decide at design time whether a user can hold several accounts for a provider; if so, store the choice on the user and pass it as account or as the chosen grantId:
import { AmbiguousGrantError } from "@alter-ai/alter-sdk";
function chooseGrant(candidates: AmbiguousGrantError["candidates"]): string { // Show each candidate's label / accountIdentifier / accountDisplayName to the // user, store the chosen grantId on the user, and reuse it. Never candidates[0]. throw new Error("present the candidates and persist the chosen grantId");}
let response;try { response = await app.request(HttpMethod.GET, url, { provider: "<provider-id>", userToken: callingUserJwt, account: savedAccount, // undefined until the user has chosen });} catch (e) { if (!(e instanceof AmbiguousGrantError)) throw e; const grantId = chooseGrant(e.candidates); // grantId together with the user's JWT keeps the grant bound to that user. response = await app.request(HttpMethod.GET, url, { grantId, userToken: callingUserJwt });}Identity mode for a managed-secret provider (by slug)
Section titled “Identity mode for a managed-secret provider (by slug)”// `provider` is the provider's per-application slug, not its template name.const response = await app.request(HttpMethod.POST, "https://api.example.com/v1/resources", { provider: "<provider-slug>", userToken: callingUserJwt, json: { action: "example" },});Direct mode with audit context
Section titled “Direct mode with audit context”await app.request( HttpMethod.GET, "https://api.example.com/v1/resources", { grantId, queryParams: { limit: 100 }, reason: "Nightly reconciliation", context: { runId, jobId, batch: "customers" }, },);Reading retry metadata
Section titled “Reading retry metadata”const response = await app.request("GET", url, { grantId });if (response.retryInfo !== null) { console.log( `Token refresh succeeded on attempt ${response.retryInfo.successfulAttempt}`, );}CLI advanced reference
Section titled “CLI advanced reference”This command requires the acknowledged organization exception above and a runtime API key (ALTER_API_KEY), never a PAT.
alter sdk-passthrough request <grant-id> --url <url> [--method <method>] [--body <json>] [--header <kv>] [--api-key <key>]request
Section titled “request”Make a single authenticated call against a grant. The grant must be active and owned by the calling app.
Required key scopes. request retrieves the credential over the token channel, calls the provider directly, then reports the outcome over the audit channel — the key needs tokens:retrieve and audit:emit. Without audit:emit the provider call still succeeds but its api-call audit row is dropped, and the CLI prints Audit log failed with status 403 (non-fatal) naming the missing scope. (Proxied calls made with proxy_request() / proxyRequest() in the SDKs need proxy:execute instead.)
| Flag | Required | Description |
|---|---|---|
--url <url> | yes | The full provider URL to call (e.g. https://www.googleapis.com/oauth2/v3/userinfo). Must be http:// or https://. There is no relative-path resolution — pass the complete URL. |
--method <method> | no | HTTP method (default GET). One of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. |
--body <json> | no | JSON text parsed locally, then passed as the provider request body. Invalid JSON exits with usage code 2 before any request. |
--header <kv> | no | Extra header in Name: value form. Repeatable; a later occurrence of the same header name overwrites the earlier value. Reserved headers (the Authorization header and Alter’s own request headers) are rejected — Alter injects the credential for you. |
--api-key <key> | no | Runtime API key, if not using ALTER_API_KEY. The CLI validates the complete format locally: scoped keys use a supported prefix, 22 lowercase-base32 random characters, and a matching 6-character CRC; legacy alter_key_… keys use exactly 40 URL-safe body characters. PAT plaintext (alter_pat_…) is rejected — use alter auth login for that. Inline values print a shell-history warning. |
The response body goes to stdout (pipe it into jq); the status line and response headers go to stderr, so the body stays clean for downstream tools.
A provider error keeps the same layout: the HTTP <status> line and an explanatory message go to stderr, the provider’s error body goes to stdout, and the command exits 1. The body is the diagnostic (a validation error naming the field, a “database resuming” notice, a rejected credential), so a script that keys off the exit code can still read it.
Stderr also carries an alter-rate-limit: line reporting Alter’s own remaining budget for the key — alter-rate-limit: 994/1000 remaining, resets 2027-01-15T08:00:00.000Z. It is namespaced so it cannot be confused with the provider’s response headers printed beside it, which describe a different budget entirely. The line is omitted when the backend reported no budget for the call.
export ALTER_API_KEY="alter_rk_..."
alter sdk-passthrough request <grant-id> \ --url https://www.googleapis.com/oauth2/v3/userinfo \ | jq .
# With a bodyalter sdk-passthrough request <grant-id> \ --method POST \ --url https://slack.com/api/chat.postMessage \ --body '{"channel":"#general","text":"hello"}'Stub applications
Section titled “Stub applications”When the grant is scoped to a stub application, the call is served by the stub provider rather than the real one. If that application is reached on a deployment where stub-provider mode is not enabled, the request is refused with conflict code 5 and a message explaining that the application cannot be served there.
That refusal is terminal, not transient: it holds for as long as the application and the deployment disagree, so retrying the same call can never succeed. Scripts should treat exit 5 here as “this application cannot run against this deployment” and stop, rather than backing off and retrying.