Ingram Cloud

Documentation

Deployments & schedules

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:

  1. 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.
  2. 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/deployments lists 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, Slack team:user, email From). 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

  1. Create a bot with @BotFather, then save its token in Settings → Integrations (PUT /v1/tenant/telegram { "bot_token": "…" }). Ingram Cloud registers the webhook itself.
  2. 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.

WhatsApp

  1. 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 a webhook_url + verify_token to paste into your Meta app's webhook config.
  2. Bind a smith with a wa.me link (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 the events_url the response returns.
  • client_id + client_secret: the same app's OAuth client; this enables the oauth_install setup below. Add the returned oauth_redirect_url to 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 via setup.return_url) to send the installer's browser back to your product with ?slack=connected|cancelled|error appended.

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).

  1. 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.

Email

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.

LinkedIn

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; DELETE removes it; POST …/schedules/{schid}/run_now fires 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).