Ingram Cloud

Documentation

Auth & tokens

Auth & tokens

Every request carries Authorization: Bearer <token>, and pins the wire with IC-Api-Version: 2026-05-01 (conventions). Tokens are RS256 JWTs; the sub claim encodes identity, so the tenant is implicit in the token and never appears in a URL.

Two token types

Tenant-adminSmith token
sub<tenant><tenant>:<smith id>
Accessevery /v1 operation, in this project onlyonly that one smith's data
Lifetimedays, or non-expiring≤ 24 hours (enforced)
Livesyour server, as a secreta browser or device is fine
Handle prefixtha_live_…thp_live_…

A tenant-admin token carries the tenant:* scope. It grants everything, but is tenant-bounded: every query the API runs filters by the token's own tenant, with no bypass, so it cannot read another project even if leaked across projects you own. Treat it like a database password regardless.

A smith token is cryptographically bound to one smith, so requests for any other smith fail with 403 smith_mismatch, by construction rather than by policy. Hand it to a browser or device so a person can talk to their smith and nothing else.

Minting tokens

Console: tenant-admin keys live at Settings → API keys; a key scoped to one smith is minted from that smith's Tokens tab (Smiths → open a smith → Tokens). Both are shown once. API: minting requires a tenant-admin token:

# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/tenant/tokens \
  -H "Authorization: Bearer $IC_TOKEN" \
  -H "IC-Api-Version: 2026-05-01" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "smith", "smith_id": "smt_…",
        "permissions": ["runs:read", "runs:write", "memories:read"],
        "ttl_seconds": 3600, "name": "browser session for user_123" }'
# → 201 { "id": "tok_…", "token": "thp_live_<jwt>",
#         "sub": "<tenant>:smt_…", "expires_at": "…" }

For a tenant-admin token: { "scope": "admin", "ttl_seconds": 7776000 } (omit ttl_seconds for non-expiring). ttl_seconds must be positive and at most 315360000 (10 years) — 0 is 422 invalid_request, not "no expiry". GET /v1/tenant/tokens lists minted tokens; DELETE /v1/tenant/tokens/{id} revokes one immediately.

Organization keys & projects

A project is a tenant — the isolation boundary a tenant:* token is bound to. One tier up is the organization (your account), which owns many projects. An organization key carries the organization:* scope with sub = your organization id. It is the master key you hand to infrastructure-as-code, and it reads no run, memory, or smith itself — it only:

  • manages projects — POST/GET /v1/organization/projects, GET/DELETE /v1/organization/projects/{pid}
  • mints a project's tenant-admin token — POST /v1/organization/projects/{pid}/tokens → a tenant:* token whose sub is that project id.
  • manages account creditsGET /v1/organization/billing/balance, …/billing/ledger, and POST …/billing/checkout (the balance is org-level, pooled across the account's projects).
# Authorization: organization key (server-side / IaC only)
curl https://api.cloud.ingram.tech/v1/organization/projects \
  -H "Authorization: Bearer $IC_ORG_TOKEN" \
  -H "IC-Api-Version: 2026-05-01" -H "Content-Type: application/json" \
  -d '{ "name": "acme" }'
# → 201 { "id": "proj_…", "organization": "<org>", "name": "acme" }

# Authorization: organization key (server-side / IaC only)
curl https://api.cloud.ingram.tech/v1/organization/projects/proj_…/tokens \
  -H "Authorization: Bearer $IC_ORG_TOKEN" \
  -H "IC-Api-Version: 2026-05-01" -H "Content-Type: application/json" \
  -d '{ "name": "acme prod" }'
# → 201 { "token": "tha_live_<jwt>", "sub": "proj_…", "scopes": ["tenant:*"] }

You mint the organization key once for your account (it is bootstrapped with your org id, like your first admin token), hand it to your Pulumi stack, and let the IcProject and IcProjectToken resources provision a project and drop its tenant:* token straight into each app's env. A leaked org key can reshape your project list and mint project tokens, but cannot read a single run — only the project tokens it mints can. Creating a project by a name that already exists returns the existing one, so re-running your IaC reconciles rather than errors.

In the console: Settings → Organization is where you rename your organization and mint organization keys. The secret is shown once at mint time and never stored — but each key you mint is listed there afterward (its id and when it was minted/expires), so you can see which keys exist. Rotation today is mint-a-new-one; there is no console revocation yet.

The browser pattern

Never ship a tenant-admin token to a client. Instead, on session start your backend mints a short-lived smith token and hands it to the browser:

browser ──login──▶ your backend ──POST /v1/tenant/tokens──▶ Ingram Cloud
browser ◀──thp_live_… (1h)──┘
browser ──POST /v1/smiths/{their id}/runs──▶ Ingram Cloud   # direct, scoped

Re-mint when it expires (the API returns 401); smith tokens are cheap and stateless.

Connector OAuth

For mcp deployments the API is also an OAuth 2.1 authorization server, so an MCP host (Claude custom connectors, ChatGPT developer-mode connectors) can obtain tokens on its own instead of being handed one. Discovery is standard: the MCP endpoint's 401 names the Protected Resource Metadata (GET /.well-known/oauth-protected-resource/v1/deployments/{depid}/mcp, RFC 9728); GET /.well-known/oauth-authorization-server (RFC 8414) lists POST /oauth/register (Dynamic Client Registration, RFC 7591), GET /oauth/authorize (the consent page), and POST /oauth/token (authorization code + PKCE S256, rotating refresh tokens).

The consent page authenticates the person with a pasted IC token, and the access tokens the flow mints are ordinary smith tokens scoped to runs:write — everything on this page (binding, revocation, expiry) applies to them unchanged. The verification key is published at GET /.well-known/jwks.json; every minted JWT carries its kid. Walkthrough: Connectors.

Permission scopes

Smith tokens carry a subset of the closed vocabulary, and permissions is required: name the scopes the token may use. There is no "grant everything" default — omitting it is 422 permissions_required, because the widest possible token is not a sane thing to get from the shortest possible request. Pass only *:read scopes for a read-only token; the console's token form has a checkbox for exactly that.

Scopes gate operations; the smith binding still confines every call to that one smith's data. Scope is never the only thing standing between a smith token and your tenant's configuration. Every endpoint also declares who may call it at all, independently of scope:

PostureWho may callRefusal
Tenant configuration — agents, the tools registry, webhooks, budgets, customers, provider credentials, messaging setup, smith creationa tenant-admin token403 tenant_token_required
Credentials — model keys, token minting/listing/revocationa tenant-admin token (the tenant:* marker itself)403 admin_required
Account — projects and creditsan organization key403 organization_required
Everything elseany token; a smith token sees only its own smith's data

So a smith token minted with every scope in the vocabulary still cannot read your webhooks, change a budget, or overwrite an OAuth client. The refusal comes before the request body is read.

Scope pairGates
runs:read / runs:writeruns and the smith resource
conversations:read / conversations:writethe conversations resource (threads of runs)
memories:read / memories:writeworking memory + recall
connections:read / connections:writeper-smith OAuth connections
deployments:read / deployments:writemessaging (deployment) endpoints
schedules:read / schedules:writecron schedules
approvals:read / approvals:writeread the approvals queue; write decides one, on every surface that can
traces:read / traces:writespan waterfalls; write gates span ingestion
usage:read / usage:writeusage rollups & budgets; write gates custom meter events and budget changes
customers:read / customers:writeyour customer objects
files:read / files:writefile metadata + bytes; write gates upload and delete
vector_stores:read / vector_stores:writevector stores and search; write gates store/file changes
model_keys:read / model_keys:writea smith's own end-user model keys
embeddings:writethe POST /v1/embeddings compute endpoint (write-only)

Missing scope → 403 insufficient_scope with the required scope in details.

Security rules

  • Tenant-admin tokens never leave your server. No exceptions, not even "temporarily" in a mobile build.
  • The API verifies RS256 signatures on every authenticated request; there is no API-key fallback.
  • Revocation is immediate (DELETE /v1/tenant/tokens/{id}), including for non-expiring admin tokens. Name your tokens so you can find the right one. Revoking an id we hold no record of is 404 not_found — never a 204 that revoked nothing.
  • Webhook payloads are authenticated separately, by HMAC signature; see Events & webhooks.
  • A few surfaces are public by design and take no token: inbound provider webhooks (authenticated by the provider's signature) and a hosted page's GET /v1/hosted/{id} + POST /v1/hosted/{id}/chat — gated by the unguessable deployment id and an optional page password, and bounded by your project budget.