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-admin | Smith token | |
|---|---|---|
sub | <tenant> | <tenant>:<smith id> |
| Access | every /v1 operation, in this project only | only that one smith's data |
| Lifetime | days, or non-expiring | ≤ 24 hours (enforced) |
| Lives | your server, as a secret | a browser or device is fine |
| Handle prefix | tha_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→ atenant:*token whosesubis that project id. - manages account credits —
GET /v1/organization/billing/balance,…/billing/ledger, andPOST …/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:
| Posture | Who may call | Refusal |
|---|---|---|
| Tenant configuration — agents, the tools registry, webhooks, budgets, customers, provider credentials, messaging setup, smith creation | a tenant-admin token | 403 tenant_token_required |
| Credentials — model keys, token minting/listing/revocation | a tenant-admin token (the tenant:* marker itself) | 403 admin_required |
| Account — projects and credits | an organization key | 403 organization_required |
| Everything else | any 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 pair | Gates |
|---|---|
runs:read / runs:write | runs and the smith resource |
conversations:read / conversations:write | the conversations resource (threads of runs) |
memories:read / memories:write | working memory + recall |
connections:read / connections:write | per-smith OAuth connections |
deployments:read / deployments:write | messaging (deployment) endpoints |
schedules:read / schedules:write | cron schedules |
approvals:read / approvals:write | read the approvals queue; write decides one, on every surface that can |
traces:read / traces:write | span waterfalls; write gates span ingestion |
usage:read / usage:write | usage rollups & budgets; write gates custom meter events and budget changes |
customers:read / customers:write | your customer objects |
files:read / files:write | file metadata + bytes; write gates upload and delete |
vector_stores:read / vector_stores:write | vector stores and search; write gates store/file changes |
model_keys:read / model_keys:write | a smith's own end-user model keys |
embeddings:write | the 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 is404 not_found— never a204that 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.