Deployments & schedules
Runs your code doesn't start. A deployment binds a messaging surface — Slack, Telegram, WhatsApp, email, or an MCP server — to a target, so an inbound message runs the target automatically and the reply goes back to the same conversation. Schedules fire runs on cron.
Integrations vs. deployments
Two layers, kept separate on purpose:
- Integrations (per project): provider credentials — a Slack app, a Telegram bot, a Cloudflare email domain — configured once under Settings → Integrations. Ingram Cloud receives their webhooks for you.
- Deployments (per smith or agent): the binding that says who answers. A
deployment is a top-level resource (
/v1/deployments), not nested under a smith —GET /v1/deploymentslists every deployment in the project.
Targets: a smith, or an agent
Every deployment names a target:
smith— one fixed smith answers. The classic binding: this chat / this inbox ⇆ this smith.agent— the kind's catch-all. Every inbound sender that doesn't match an exact smith binding gets their own freshly minted smith of that agent, keyed by their provider identity (Telegram user, WhatsApp number, Slackteam:user, emailFrom). Same person next time → same smith, with its own memory and threads. One agent catch-all per(kind)— exact smith bindings always win.
{ "target": { "type": "smith", "id": "smt_…" }, "kind": "telegram", "setup": { "mode": "start_token" } }
{ "target": { "type": "agent", "id": "agt_…" }, "kind": "whatsapp" }
A tenant token may target any smith or agent in the project; a smith token may only target its own smith.
Once a deployment is bound, the smith also gains the matching outbound send tool
automatically (telegram_send_message / whatsapp_send_message /
slack_send_message / email_send) so scheduled or proactive runs can reach out
first.
Verified sender identity
Smiths are reachable by other people (anyone can DM a Slack app or email an
inbox), so every deployment-initiated run carries a verified sender block, in
the run's metadata.sender and as a platform line the model sees before the
message:
{ "is_owner": true, "channel": "slack", "identity": "slack:U0123ABC",
"display": "Dana Vex", "verified": true }
identity is always the channel-authenticated id, never a display name.
is_owner is derived per kind: Slack: the sender's user id equals the OAuth
installer's; email: the From address matches the deployment's owner_email
and DMARC passed (verified carries the auth verdict); Telegram: always the
owner (1:1 deep link binding). Lines in the inbound text that mimic the platform
preamble are stripped before the real one is prepended, so a visitor can't type
their way into owner status. Build gatekeeper behaviour on this: act for the
owner, take messages from visitors. (Per-caller minted smiths from an agent target
each own their smith, so they read as the owner.)
Set the owner's address on an email deployment at provision
("setup": { "owner_email": "dana@example.com" }) or later via
PATCH /v1/deployments/{id}.
Renaming a deployment's identity
PATCH /v1/deployments/{id} with { "display_name": "…" } renames the smith
where the deployment shows a name: a provisioned Slack app is renamed in Slack
itself (apps.manifest.update), and an email deployment changes its From
display name. Other kinds return 422 rename_unsupported.
Quick-reply chips
A smith on a chat channel can offer quick replies — short tappable labels
shown alongside its message. Tapping one sends that label back as the user's next
message. The agent calls a suggest_replies tool (auto-enabled whenever the smith
has a Telegram or WhatsApp deployment); pass replies as a few short labels:
// the agent's tool call, mid-run
{ "name": "suggest_replies", "replies": ["Yes, book it", "Not now"] }
Declare them once and each channel renders natively — no per-channel layout in your app:
- Telegram — a one-time reply keyboard; the tap sends the label as a normal message.
- WhatsApp — interactive reply buttons (up to 3; more than 3 fall back to a text list).
- Slack / email / LinkedIn — no native chip widget, so the labels degrade to a short bulleted list under the message (the user just types their pick).
The offered labels also ride the run record: run.output.suggested_replies and the
message.completed event carry them, so a tap is auditable. The proactive send
tools (telegram_send_message, whatsapp_send_message) take the same
suggested_replies field, so an outbound reminder can carry chips too.
Chips are for clear choices (a Yes/No, a short menu), not open questions — keep labels under 20 characters.
Telegram
- Create a bot with @BotFather, then save its token in Settings →
Integrations (
PUT /v1/tenant/telegram { "bot_token": "…" }). Ingram Cloud registers the webhook itself. - Bind a smith by deep link:
# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/deployments \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "target": { "type": "smith", "id": "smt_…" },
"kind": "telegram", "setup": { "mode": "start_token" } }'
# → { "deep_link": "https://t.me/your_bot?start=…", … }
Send that link to the person; tapping it binds their Telegram account to their
smith. To let anyone who DMs the bot get their own smith instead, deploy an
agent catch-all — no link, no setup:
POST /v1/deployments { "target": { "type": "agent", "id": "agt_…" }, "kind": "telegram" }.
An approval pause arrives as an inline keyboard with ✅ Approve, ❌ Reject,
and ✏️ Edit; the tap routes back through /submit. Edit rejects the proposed
call, then runs your next message as a correction in the same thread.
- Register your WhatsApp Cloud API number in Settings → Integrations
(
PUT /v1/tenant/whatsapp { "phone_number_id": "…", "access_token": "…", "app_secret": "…" }). Ingram Cloud validates it with Meta and returns awebhook_url+verify_tokento paste into your Meta app's webhook config. - Bind a smith with a
wa.melink (WhatsApp has no/start, so the link's prefilled text carries a connect token):
# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/deployments \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "target": { "type": "smith", "id": "smt_…" },
"kind": "whatsapp", "setup": { "mode": "start_token" } }'
# → { "deep_link": "https://wa.me/15550100?text=Connect%20my%20account%20…", … }
The person's first message (carrying the prefilled token) binds their number to
their smith and fires deployment.bound. An approval pause is delivered as
WhatsApp interactive Approve / Reject buttons routed back through /submit.
An agent catch-all ("target": { "type": "agent", … }, no setup) mints a smith
per phone number instead.
24-hour window. Freeform sends work inside WhatsApp's 24-hour customer-service window. Outside it, Meta requires a pre-approved template; template sending is not built yet, so a freeform send outside the window fails and surfaces the Graph error.
Slack
Slack supports two identity models on the one slack kind. Shared bot: one
Slack app speaks for the project; many smiths bind to it, one per Slack
conversation. Per-smith app: each smith gets its own Slack app (its own
name, avatar, and DM) minted on demand from your manifest template.
Project setup
Configure either or both blocks with PUT /v1/tenant/slack:
# Authorization: tenant-admin token (server-side only)
curl -X PUT https://api.cloud.ingram.tech/v1/tenant/slack \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "bot_token": "xoxb-…", "signing_secret": "…",
"client_id": "…", "client_secret": "…" }'
bot_token+signing_secret: the shared app. Point its Events API request URL at theevents_urlthe response returns.client_id+client_secret: the same app's OAuth client; this enables theoauth_installsetup below. Add the returnedoauth_redirect_urlto the app's OAuth settings.factory: a separate block enabling per-smith apps, minted from your own Slack account's App Configuration Tokens. Apps minted from it belong to your Slack account; the template is fixed, and creation substitutes only${display_name}and${owner_name}, with event/redirect URLs always forced to Ingram Cloud. Add"return_url": "https://yourapp.example/dashboard"(top-level or per deployment viasetup.return_url) to send the installer's browser back to your product with?slack=connected|cancelled|errorappended.
Binding a smith
# Authorization: tenant-admin token (server-side only)
# 1. OAuth install (shared bot), "Add to Slack", no ids to copy:
curl -X POST https://api.cloud.ingram.tech/v1/deployments \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "target": { "type": "smith", "id": "smt_…" },
"kind": "slack", "setup": { "mode": "oauth_install" } }'
# → 201 { "status": "pending", "install_url": "https://slack.com/oauth/v2/authorize?…", … }
# Authorization: tenant-admin token (server-side only)
# 2. Provision a per-smith app (needs the factory block):
curl -X POST https://api.cloud.ingram.tech/v1/deployments \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "target": { "type": "smith", "id": "smt_…" },
"kind": "slack", "setup": { "mode": "provision", "display_name": "Alice'\''s PA" } }'
# → 201 { "status": "pending", "slack_app_id": "A0…", "install_url": "…", … }
Send the install_url to the person. One consent grants both tokens; then the
deployment flips active bound to their DM, and slack.install +
deployment.bound fire. Install links are one-time. To install the same
minted app into another workspace, create another deployment with
"setup": { "mode": "provision", "app_id": "A0…" }. Minting is capped per project
(50 apps by default; 429 slack_app_cap_reached beyond it).
- Or bind a literal conversation id (the original way):
POST /v1/deployments { "target": { "type": "smith", "id": "smt_…" }, "kind": "slack", "address": "C…" }.
What wakes the smith
DMs always wake the bound smith. In group channels the bot answers only when
@-mentioned; a deployment with "provider_metadata": { "wake": "all_messages" }
opts back into waking on every message. Replies are thread-aware. Provisioned apps
can enable Slack's assistant pane (assistant:write +
assistant_thread_started), with a per-deployment greeting and suggested prompts
via provider_metadata.
Slack tools the smith gains
Every bound deployment adds slack_send_message. An installed app also adds, gated
by the tokens it holds: slack_search (search.messages, user token),
slack_read_messages, slack_list_channels. Approvals pause runs and post
Approve/Reject buttons in the conversation, resolved through /submit
(Tools & approvals).
GET /v1/deployments lists deployments; DELETE /v1/deployments/{id} unbinds. If
a workspace uninstalls an app, the deployment flips revoked and
slack.uninstalled fires.
Each smith can own a real inbox on your domain (BYO Cloudflare). Configure the
sending domain once (PUT /v1/tenant/email), then mint an inbox for a smith:
POST /v1/deployments { "target": { "type": "smith", "id": "smt_…" }, "kind": "email", "setup": { "mode": "provision", "handle": "jo" } }
→ an active deployment at jo@your-domain. Inbound mail wakes the smith; its
threaded reply comes back from the same address; email_send lets it write first.
An agent catch-all ("target": { "type": "agent", … }, no setup) answers any
inbound mail that reaches Ingram Cloud, minting a smith per From address.
Status: pending LinkedIn Partner Program approval. The config and binding are
wired, but no message moves until LinkedIn grants Pages-messaging API access and the
operator enables the transport — messaging_enabled on GET /v1/tenant/linkedin
reports whether it's live.
Configure your LinkedIn app once (PUT /v1/tenant/linkedin { "client_id": "…", "client_secret": "…", "page_urn": "urn:li:organization:…" }); Ingram Cloud holds the
secret encrypted and exposes an inbound receiver at /v1/linkedin/webhook/{tenant}.
Bind a Page as an agent catch-all
(POST /v1/deployments { "target": { "type": "agent", "id": "agt_…" }, "kind": "linkedin" }):
once messaging is live, an inbound Page message mints a smith per member and wakes it,
the threaded reply returns to the same conversation, and linkedin_send_message lets
the smith write first. LinkedIn DMs carry no inline buttons, so an approval.required
pause is delivered as a text prompt — the member replies "yes" to approve.
MCP server
Deploy a smith or agent as an MCP server so any Model Context Protocol client (Claude Desktop, an IDE agent, another framework) can call it as a tool. It's the mirror of registering an MCP tool server: there Ingram Cloud is the MCP client; here it's the server.
# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/deployments \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "target": { "type": "agent", "id": "agt_…" }, "kind": "mcp" }'
# → 201 { "id": "dep_…", "kind": "mcp", … }
The server lives at POST /v1/deployments/{id}/mcp (Streamable HTTP, JSON-RPC
2.0). Point an MCP client there with an Ingram bearer token. It exposes:
- one tool,
ask— send a message, get the smith's reply (the smith keeps its memory and history across calls); - read-only resources — the smith's memory entries (
memory://…).
A smith target exposes that one shared smith; an agent target mints a persistent smith per caller (keyed on the token subject), so distinct caller tokens get isolated smiths. (Full MCP OAuth discovery is coming; a configured bearer token is a first-class MCP auth mode today.)
Schedules
Cron-triggered runs, per smith (check-ins, digests, reminders):
# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/smiths/smt_…/schedules \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "name": "morning briefing", "cron": "0 8 * * *",
"timezone": "Europe/Athens",
"input": [{ "role": "user",
"content": "Compose my morning briefing: calendar, news, reminders." }] }'
input is what the smith receives when the cron fires, same shape as a
run's input. The reply reaches the person through their bound
deployment's send tool (or sits on the run record if none is bound).
PATCH …/schedules/{schid} { "enabled": false }pauses one;DELETEremoves it;POST …/schedules/{schid}/run_nowfires it immediately.- A scheduled run is a normal run: it shows up in Runs, streams events to the feed, and (via send tools) can message the person on its own.
In the console
Deployments (in the sidebar) lists every deployment in the project with its
target, kind, address, and status. A smith's Deployments tab creates Telegram
deep links, Slack install links (both setup modes), and email inboxes; its
Schedules tab manages cron entries with a run-now button. You connect each
provider once for the whole project under Settings → Integrations (per-smith
Slack identities, the factory block, live there too).