Smiths
A smith is one of your end-users: the unit of identity, memory, and data isolation. Each one is that person's running clone of an agent, holding their own memory, threads, and connections. (Yes, we call them Smiths 🕶️.) Create one per person from your backend; everything personal (conversations, remembered facts, OAuth connections, deployments, schedules) belongs to it.
Creating smiths
# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/smiths \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: smith-create-user_123" \
-d '{ "external_id": "user_123", "display_name": "Ada Lovelace" }'
# → 201 { "id": "smt_…", "external_id": "user_123",
# "config_source": "agent", "agent_id": "agt_…", "model": "default", … }
external_idis your own user id. A smith's identity is the pair (external_id,agent_id): one end-user gets one live smith per agent, so the same person can run several agents (an accountant, a tax advisor, an auditor) as several smiths. Creating is upsert-by-(external_id,agent_id): if a live smith already has that pair, the call returns it (200, unchanged) instead of erroring, so "ensure smith" on every login is safe with or without anIdempotency-Key. (The key still gives an exact replay of the original201for 24h.) A new pair returns201. Omitagent_idand it resolves to your default agent, so single-agent apps keep getting one smith per user.GET /v1/smiths?external_id=user_123looks one up by your id;GET /v1/smiths?agent_id=agt_…lists the smiths cloning one agent;GET /v1/smithslists all (cursor-paginated);DELETE /v1/smiths/{id}soft-archives. Deleting frees theexternal_id. Re-creating with the same value provisions a brand-new smith (newsmt_…id), so a wiped smith can always be re-provisioned.- Optional create fields:
locale,timezone,metadata(free-form JSON),customer_id(the billable party, see Customers & metering),auto_memory(setfalsefor a stateless smith), and the config fields below.
Identity, namespaces & segments
external_id is your key for a smith, and it's immutable once set, so
choose it deliberately and namespace it — user:… for signed-in people,
anon:… for logged-out visitors (a random token you mint and cookie). Anonymous
visitors are smiths too: one per visitor keeps their threads (and memory, if you
leave it on) isolated, exactly like a signed-in user.
Namespacing pays off when you segment. external_id_prefix matches one
namespace, on both the smith list and the run feed:
# Authorization: tenant-admin token (server-side only)
# Every logged-out visitor:
curl "https://api.cloud.ingram.tech/v1/smiths?external_id_prefix=anon:" \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01"
# Every anonymous conversation (the segment's run history):
curl "https://api.cloud.ingram.tech/v1/runs?external_id_prefix=anon:" \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01"
The console's Smiths page filter box takes the same prefix. There is no
is_anonymous flag — the namespace is the segment.
When an anonymous visitor signs in, don't rename external_id (it can't change):
keep the smith and PATCH the real identity onto it (display_name,
customer_id, metadata), so its threads and memory carry over. For the full
patterns — smith vs thread, orgs as customers, the claim flow — see
Modeling your users and
Build a support agent.
How effective config resolves
Every smith runs a clone of an agent: instructions, model,
enabled_hosted_tools, mcp_servers, auto_memory. Name one in agent_id at
create, or omit
it and the smith clones your project's default agent (auto-created on first
use, then editable like any agent). A smith is never agent-less. Where that
config comes from is the config_source field on the smith:
config_source | Meaning |
|---|---|
agent | config comes from an agent version, by reference |
override | agent version + this smith's sparse overrides on top |
The resolution rule, applied at request time:
effective = agent_version(pin ?? rollout(smith_id)) + overrides
GET /v1/smiths/{id} always returns the effective config flattened on the
resource. The fields are what the smith actually runs with, whatever the
source. agent_id, agent_version (the version that resolved), and pin
ride alongside. override_keys lists which fields this smith overrides (the
rest track the agent), and inherited carries the agent version's value for
each — what a field reverts to when you clear its override.
instructions is the raw template; if the agent uses
per-smith variables ({{ user.display_name }},
declared {{ … }}), rendered_instructions is what this smith actually runs
with, resolved against its own fields and metadata. That's how you keep
identity in the prompt with zero overrides, so the smith still tracks
rollouts.
Per-smith overrides
On an agent-attached smith, PATCH /v1/smiths/{id} with config fields writes
overrides: a sparse per-smith diff. The agent is never modified from a
smith:
# Authorization: tenant-admin token (server-side only)
curl -X PATCH https://api.cloud.ingram.tech/v1/smiths/smt_… \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "instructions": "Also: this customer is on the enterprise plan." }'
# → config_source: "override"; model/tools still track the agent
Overridden fields stick through rollouts; everything else follows the agent.
The response's override_keys tells you which fields drifted.
"model": "default" means the platform's default model, fine for a first
run, but set an explicit model (see GET /v1/tenant/models for the catalog
your keys unlock) before relying on behaviour.
To drop an override and re-inherit the agent's value, PATCH that field as
null:
# Authorization: tenant-admin token (server-side only)
curl -X PATCH https://api.cloud.ingram.tech/v1/smiths/smt_… \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "model": null }'
# → model drops out of override_keys; it now tracks the agent version again
null removes that field from the overrides; only the field you send is
touched. It's distinct from "model": "default" (an explicit override to the
platform default) and from "instructions": "" (an override to empty) — those
set a value, null clears one.
In the console
A smith's Agent tab tags each config field as inherited from agent or overridden. Overridden fields show a Revert to agent control that clears the override in place.
Attaching, detaching, pinning
- Attach:
PATCH /v1/smiths/{id} { "agent_id": "agt_…" }. The smith jumps to the agent's live version (overrides reset). To adopt existing smiths without changing their behaviour, use the agent's/attachendpoint instead; it stores any drift as overrides (Agents). - Re-home:
{ "agent_id": "" }moves the smith onto your default agent, keeping its current effective config as overrides so behaviour doesn't change. (Smiths are never agent-less.) - Pin:
{ "pin": 3 }locks the smith to version 3 regardless of rollouts;{ "pin": null }returns it to the rollout. Use pins for change-control customers or to reproduce a bug against an old version.
Config history and rollback
Every change to how a smith runs (config edit, override, attach, detach, pin) snapshots the effective config as an immutable revision:
# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/smiths/smt_…/revisions
# → { "data": [ { "revision": 4, "snapshot": { "instructions": …, "model": … },
# "created_by": "…", "created_at": "…" }, … ],
# "next_cursor": null, "has_more": false }
curl -X POST https://api.cloud.ingram.tech/v1/smiths/smt_…/revisions/2/restore
A revision is a smith's config-history snapshot, distinct from an agent's published version. History is append-only: a restore re-applies the old snapshot as a brand-new revision. On an agent-attached smith, restore writes the snapshot as overrides. The attachment survives. (Fleet-level history lives on the agent's own versions.)
OAuth connections
A connection lets a smith act on that person's external accounts (Google,
Stripe, …). It is the per-smith credential an oauth MCP tool
forwards, so the tool acts strictly as that human. There are two ways to create
one: the hosted connect flow (Ingram Cloud drives the consent dance — the
easy path, and the only one for catalog
presets) or pushing a token you obtained yourself.
Connecting an end-user's account
Ask Ingram Cloud for an authorize URL for the smith, send the person to it, and IC handles the redirect, exchanges the code, and vaults the token:
# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/smiths/smt_…/connections/authorize \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "provider": "stripe", "return_url": "https://your.app/connected" }'
# → 200 { "authorize_url": "https://…", "provider": "stripe" }
Open authorize_url in the person's browser. After they consent, the provider
redirects to Ingram Cloud's GET /v1/oauth/callback/{provider}; IC stores the
connection and 302s back to your return_url with ?connection=ok (or
error). When a tool call needs a connection the smith doesn't have yet, the
connection.required event already carries a ready-to-use
authorize_url — surface it, then submit the paused run to
continue.
Whose OAuth client brokers consent is the server's auth.client_mode. The
platform mode (default for catalog presets) uses an Ingram-owned client, so
you register nothing — the consent screen reads "Ingram Cloud". The tenant
mode uses your own client (configure it once at
PUT /v1/tenant/providers/{provider}), so the consent screen carries your
brand. Either way the redirect URI and token vault are Ingram Cloud's.
Pushing a token you obtained yourself
If you'd rather run the OAuth dance under your own client (e.g. a provider not in the catalog), do so and push the resulting grant:
- Configure your OAuth client once per provider:
PUT /v1/tenant/providers/google { "client_id": …, "client_secret": …, "scopes_allowed": […] }(console: Settings → OAuth apps). The PUT replaces every field you send, with one exception:client_secretis write-only and sticky. Omit it and the stored secret is kept (so you can edit scopes without re-sending it); send a new value to replace it, or sendnullto clear it. AGETreports onlyhas_client_secret, never the value. - After the person consents in your app, push their grant:
# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/smiths/smt_…/connections \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "provider": "google", "scopes": ["calendar.readonly"],
"credential": { "access_token": "…", "refresh_token": "…",
"expires_at": "…" } }'
POST /v1/smiths/{sid}/connections/{cid}/refreshrefreshes using your stored client credentials;GETlists (credentials are write-only and never read back);DELETErevokes the smith's access.
How tokens stay fresh
Refresh is lazy: when a tool call needs the token and it is within 60 seconds
of expires_at (or past it), Ingram Cloud refreshes it in place before the
call. There is no background sweep — a token can sit expired between runs and
self-heals on the next one. POST …/connections/{cid}/refresh forces a refresh
any time.
Both token_uri and refresh_webhook are fetched by Ingram Cloud's servers, so
both must be public https endpoints — a private, loopback, or link-local target
is rejected with invalid_url (422).
How the refresh runs depends on your provider config:
- Stored client (a
client_secretis on the provider): Ingram Cloud postsgrant_type=refresh_tokento the provider'stoken_uriand stores the new token. - Delegated (
refresh_webhookset, noclient_secret): your OAuth client never leaves your servers. Ingram Cloud POSTs{ "connection_id": "con_…" }to yourrefresh_webhook; refresh under your own client and PATCH the new credential back before responding — the pending tool call re-reads the connection as soon as your webhook returns (10-second timeout):
# Your server, handling the refresh webhook
# Authorization: tenant-admin token (server-side only)
curl -X PATCH https://api.cloud.ingram.tech/v1/smiths/smt_…/connections/con_… \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "credential": { "access_token": "…",
"expires_at": "2026-07-16T15:00:00Z" } }'
The PATCH merges into the stored credential, so a token-only refresh need not
resend the refresh_token.
Connections feed your MCP tools: register an MCP server with
auth.kind: "oauth" and Ingram Cloud forwards the matching provider's token to
your server on each call, so the tool acts as the smith.
In the console
A smith's Connected accounts tab lists its connections and drives the hosted connect flow: pick a configured OAuth app and Connect account sends you through that provider's consent screen, landing back on the tab. Pasting a token you already hold sits behind Advanced — paste a token. (Configure the OAuth apps you can connect through in Settings → OAuth apps.) Don't confuse it with the API tokens tab, which mints inbound bearer tokens for calling the API as this smith.
End-user keys
A smith can carry its own model-provider key, so an end-user's inference bills their provider account, not yours. The smith key wins over your tenant BYOK key and the Ingram-hosted key (see Models — key resolution), and your wallet is charged the platform margin only for that turn — the provider cost sits on the end-user's account. See Billing.
Keys are per-provider, live beside the smith (not part of its config, so setting one records no revision), and are never echoed — a read reports presence only.
Setting a key targets an existing smith by id, so provision first, then set — two server-side calls, both idempotent:
# Authorization: tenant-admin token (server-side only)
# 1. Ensure the smith exists and learn its id (upsert-by-(external_id, agent_id)).
curl https://api.cloud.ingram.tech/v1/smiths \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "external_id": "user_123", "agent_id": "agt_…" }'
# → { "id": "smt_…", … } (200 if it already existed, 201 if just created)
# 2. Set the end-user's key on the returned id.
curl -X PUT https://api.cloud.ingram.tech/v1/smiths/smt_…/model_keys/openai \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "api_key": "sk-…" }'
# → { "provider": "openai", "configured": true, "base_url": null }
The smith the key lands on is exactly the one that end-user's runs resolve to —
both provisioning paths key on the same (external_id, agent_id) identity, so a
key set before the first chat is already in place when it starts.
GET /v1/smiths/{id}/model_keyslists the configured providers (presence +base_url, never the key).PUT /v1/smiths/{id}/model_keys/{provider}takes{ api_key, base_url? }(base_urlonly where the provider allows an OpenAI-compatible endpoint override, i.e.openai). Setting a key over an existing one replaces it.DELETE /v1/smiths/{id}/model_keys/{provider}removes it; the smith falls back to your tenant/hosted key.
Scopes are model_keys:read / model_keys:write (see Auth). A
smith-scoped token carries them by default, so an end-user's browser can set
their own key directly — mint a smith token for the (already-provisioned) smith
and the key never rests on your server.
A present-but-invalid smith key fails that smith's runs with the provider's auth error rather than silently falling back to your key. Removing the key restores the fallback.
Applies to a smith's run inference. Memory consolidation, recall embeddings, and vector-store operations resolve your tenant/hosted key regardless.