Guides
Provision Secrets for Backend Services
Store API keys and service tokens in Alter instead of environment variables, scoped to principals.
By the end of this guide, a backend service calls a third-party API using an operator-provisioned credential — a Datadog API key, a Stripe secret key, an AWS access key — without that credential ever appearing in the application’s environment, config files, or logs.
The flow:
- The operator stores the credential once in the developer portal.
- The operator issues a grant binding the credential to a principal (system, user, group, or agent).
- The backend calls
app.request()with the resultinggrant_id. Alter injects the credential into the outgoing call.
Prerequisites
Section titled “Prerequisites”- An app with a runtime API key (
alter_rk_…, or legacyalter_key_…). - A managed-secret provider configured — this guide uses Datadog as the example.
- The credential itself, generated at the provider. For Datadog, create an API key and, for API operations that require user-level authorization, an application key.
Walkthrough
Section titled “Walkthrough”1. Store the credential
Section titled “1. Store the credential”In the developer portal:
- Open the app and go to Managed Secrets.
- Pick Datadog from the catalog (or Custom for anything not listed).
- Paste the API key into the Credential field.
- If the intended Datadog operations require an application key, paste it into Application Key. Alter stores and injects it separately.
- Click Store.
The plaintext is encrypted and sent to the vault. The portal will not display it again.
2. Issue a grant
Section titled “2. Issue a grant”A stored credential is not usable until a grant binds it to a principal. From Managed Secrets → Datadog → Grants → New Grant:
- System — the credential is callable by anyone holding the app key, with no user identity required. Use this for backend services and cron jobs.
- User — bind to one named user (resolved from their JWT). Useful when each user gets a per-user Datadog key.
- Group — bind to an IDP group. All current and future members of the group can use it. Only available when the identity provider supports group grants (Clerk, or Okta/WorkOS with stable-ID group keying — see the support table) and its webhook integration is enabled. (Auth0 group grants are not currently supported.)
- Agent — bind to a managed agent. Only that agent can reach the credential.
Agent-bound grants are created through the developer portal or the Alter CLI. The SDK’s create_managed_secret_grant / createManagedSecretGrant methods create user-, group-, or system-bound grants; an agent principal is rejected.
For a system-bound grant, the portal returns a grant_id. Copy it into application config.
Equivalent SDK call (operator path):
import asyncio, osfrom alter_sdk import Appfrom alter_sdk.models import SystemPrincipal
async def main(): async with App(api_key=os.environ["ALTER_API_KEY"]) as app: result = await app.create_managed_secret_grant( managed_secret_id="11111111-2222-3333-4444-555555555555", principal=SystemPrincipal(label="prod-monitoring"), ) print(result.grant_id)
asyncio.run(main())import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey: process.env.ALTER_API_KEY! });
try { const result = await app.createManagedSecretGrant("11111111-2222-3333-4444-555555555555", { principal: { type: "system", label: "prod-monitoring" }, }); console.log(result.grantId);} finally { await app.close();}3. Call the provider
Section titled “3. Call the provider”The application calls request() against the provider, passing the grant_id:
import asyncio, osfrom alter_sdk import App, HttpMethod
async def main(): async with App(api_key=os.environ["ALTER_API_KEY"]) as app: response = await app.request( HttpMethod.POST, "https://api.datadoghq.com/api/v1/events", grant_id=os.environ["DATADOG_GRANT_ID"], json={"title": "Deployment", "text": "Service deployed to prod"}, ) print(response.status_code, response.json())
asyncio.run(main())import { App, HttpMethod } from "@alter-ai/alter-sdk";
const app = new App({ apiKey: process.env.ALTER_API_KEY! });
try { const response = await app.request( HttpMethod.POST, "https://api.datadoghq.com/api/v1/events", { grantId: process.env.DATADOG_GRANT_ID!, json: { title: "Deployment", text: "Service deployed to prod" }, }, ); console.log(response.status, await response.json());} finally { await app.close();}The Datadog API key is injected as DD-API-KEY: …; when configured, the application key is injected as DD-APPLICATION-KEY: …. The public SDK response never exposes either value. Because request() uses retrieve mode, the SDK briefly holds and injects the credential inside the application process. Use proxy_request() when the credential must remain outside that process.
Patterns
Section titled “Patterns”Per-user managed secrets
Section titled “Per-user managed secrets”For a product where each user has a distinct API key at a provider, the operator provisions a separate managed secret and user-bound grant for each user. Managed-secret storage remains an operator action; the end user can later consent to delegate an existing user-bound grant to an agent, but does not upload the underlying credential through Connect. At call time, the SDK resolves the user-bound grant from the user’s JWT exactly as it would for an OAuth grant — see Call APIs on behalf of users.
Instead of tracking a grant_id per user, you can resolve the current user’s secret by its slug — the unique identifier shown on the secret’s page (e.g. stripe-production):
response = await app.request( HttpMethod.POST, "https://api.stripe.com/v1/charges", provider="stripe-production", # the secret's slug, unique per app user_token=current_user_jwt, # the end-user's identity token json={"amount": 1000, "currency": "usd"},)const response = await app.request(HttpMethod.POST, "https://api.stripe.com/v1/charges", { provider: "stripe-production", userToken: currentUserJwt, json: { amount: 1000, currency: "usd" },});The slug resolves to exactly one secret, so this is unambiguous. If a single user holds several secrets, enumerate them with list_grants() (scope to the user with end_user_token=…) — each item carries its managed_secret_slug and grant_id — then call by the specific grant_id. (Filtering list_grants(provider_id="stripe-production") narrows to that one slug, so it doesn’t enumerate the others.)
Rotation
Section titled “Rotation”When the credential at the provider rotates:
- Generate the new credential at the provider.
- Open Managed Secrets → [Provider] → Update credential.
- Paste the new value and save.
Every existing grant_id keeps working — the credential is replaced in place, the grant identity is unchanged. No application restart, no env-var update.
AWS managed secrets
Section titled “AWS managed secrets”AWS managed secrets carry extra structure because Alter computes SigV4 signatures per request. See AWS managed secret reference.
Group-bound managed secrets
Section titled “Group-bound managed secrets”For a “support team” Datadog key, bind the grant to an IDP group. The external_group_id is the IDP-side stable group identifier — a Clerk or WorkOS organization ID (org_…) or an Okta group ID (00g…):
from alter_sdk.models import GroupPrincipal
await app.create_managed_secret_grant( managed_secret_id="11111111-2222-3333-4444-555555555555", principal=GroupPrincipal( external_group_id="00gabc123def456", # the IDP's stable group id (e.g. Okta 00g…) idp_id="11111111-2222-3333-4444-555555555555", # the AppIdentityProvider id (UUID) label="support-team", ),)await app.createManagedSecretGrant("11111111-2222-3333-4444-555555555555", { principal: { type: "group", externalGroupId: "00gabc123def456", // the IDP's stable group id (e.g. Okta 00g…) idpId: "11111111-2222-3333-4444-555555555555", // the AppIdentityProvider id (UUID) label: "support-team", },});Any user whose JWT group claim carries that group ID can now resolve this grant under JWT identity.
Troubleshooting
Section titled “Troubleshooting”| Error | Likely cause | Fix |
|---|---|---|
GrantNotFoundError | grant_id is wrong or the grant was revoked. | Re-issue the grant from the portal. |
ProviderAPIError (401) | The credential at the provider was revoked or expired. | Generate a new credential at the provider, update the stored value. |
ProviderAPIError (403) | The credential lacks permission for the route. | Check the credential’s permission scope at the provider. |
| Application can read the credential | Misconfiguration. | Managed secrets are write-only; if anything in the application has plaintext access, the storage path is wrong. |