Ingram Cloud

Documentation

Events & webhooks

Events & webhooks

Everything that happens in a project lands on one append-only event feed: run lifecycle, approvals, tool calls, memory consolidation, budget alerts. You consume it two ways: poll the feed, or register webhooks and receive the same envelope as signed POSTs.

The event envelope

Both delivery modes carry the identical shape:

{ "v": 1, "id": "evt_…", "type": "run.completed",
  "created_at": "2026-06-11T…Z", "tenant_id": "…",
  "smith_id": "smt_…", "data": { "stop_reason": "end_turn", "usage": { … } } }

Polling the feed

# Authorization: tenant-admin token (server-side only)
curl "https://api.cloud.ingram.tech/v1/events?type=run.completed&smith_id=smt_…&since=2026-06-10T00:00:00Z&limit=50"

The feed is the reliable source of truth: if a webhook delivery is ever missed, everything is still here. The console's Observe → Events renders this same feed with the exact payload per row.

Registering webhooks

# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/tenant/webhooks \
  -H "Authorization: Bearer $IC_TOKEN" \
  -H "IC-Api-Version: 2026-05-01" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://you.example/ic/webhook",
        "events": ["run.completed", "run.failed", "approval.required"] }'
# → { "id": "wh_…", "secret": "whsec_…" }   ← shown ONCE; store it
  • An empty events list subscribes to everything; otherwise a type matches exactly or by family prefix. Subscribing to slack catches every slack.* type.
  • POST /v1/tenant/webhooks/{id}/test fires a sample event; PATCH { "active": false } pauses deliveries; DELETE removes the endpoint.

Verifying signatures

Every delivery carries X-IC-Event-Id (dedupe key) and X-IC-Signature: t=<unix ts>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>." + raw_body):

import hashlib, hmac

def verify(headers, raw_body: bytes, secret: str) -> bool:
    t, v1 = (kv.split("=", 1)[1] for kv in headers["X-IC-Signature"].split(","))
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Compute over the raw request body (before any JSON parsing), and reject timestamps older than a few minutes to block replays.

Your tools themselves run over MCP: Ingram Cloud calls your MCP server directly (it is the MCP client), so tool execution does not ride this event feed; only the human-in-the-loop and onboarding moments do (approval.required, connection.required, unbound_message). The envelope's top-level smith_id is which smith the event belongs to.

Delivery semantics

  • Delivery is one signed attempt per event today. There is no automatic retry queue yet. Build on the combination: webhooks for latency, the /v1/events poll mirror for completeness (e.g. a periodic reconciliation sweep with since=).
  • Use X-IC-Event-Id to dedupe; deliveries are not guaranteed to arrive in order.
  • Respond 2xx quickly (within 10s) and do real work async.

Event type catalog

TypeFired whenNotable data
run.starteda run beginssmith_id, thread_id
run.pausedwaiting on a tool result or approvalreason, tool_calls
run.completeda run finishesstop_reason (+ usage on sync runs)
run.faileda run errorserror
run.cancelledcancelled via /submit, or a dropped OpenAI-compatible streamreason
tool.executingthe agent invoked a server-side tool (also a live stream frame)tool, tool_call_id, args
tool.completeda server-side tool returned (also a live stream frame)tool, tool_call_id, output
approval.requireda gated tool wants to runapproval_id, tool, args
approval.resolveda human decidedapproval_id, decision, actor
connection.requiredan oauth MCP tool ran but the smith has no connectionprovider, mcp_server, authorize_url (when a connect flow is available; else null)
unbound_messagea message from an unbound sender (no smith)kind, sender, text (preview)
message.completeda turn's final textcontent, suggested_replies (when the agent offered quick-reply chips)
budget.thresholda budget crossed 80% / 100%budget_id, tier, spent
credit.exhausteda run was refused because the org wallet is emptyorganization, balance_cents
deployment.bounda deferred deployment binding completedkind, channel_id, address
deployment.inboundan inbound message woke the smithkind, channel_id, triggered_run_id
slack.app_provisionedthe factory minted a per-smith Slack appchannel_id, slack_app_id, display_name
slack.installan OAuth install completedchannel_id, team_id, slack_user_id, scopes, user_scopes
slack.uninstalleda workspace uninstalled the appchannel_id, team_id
email.send_failedan outbound email could not be deliveredchannel_id, to, detail
webhook.testyou called the test endpointsample payload