Auth & tokens
Every request carries Authorization: Bearer <token> and
IC-Api-Version: 2026-05-01. 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). 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.
Permission scopes
Smith tokens carry a subset of the closed vocabulary; omitting permissions
grants all of it (still bound to the one smith). 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. Tenant-level configuration (agents, the tools registry,
webhooks, model keys, token minting) additionally requires a tenant-admin
token: a smith token gets 403 tenant_token_required there no matter its
scopes.
| 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 | archival memories + core blocks |
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 | the approvals queue |
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 | metadata + bytes of files inlined into a conversation |
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 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. - Webhook payloads are authenticated separately, by HMAC signature; see Events & webhooks.