channel docs

channel / guides / webhooks

Mount webhooks.

Every platform pushes inbound messages to you over HTTP. The runtime gives you one pre-built handler per configured platform — your job is a single catch-all route that dispatches by path segment. Verification, parsing and persistence are handled inside.

The catch-all route

createChannels returns channels.webhooks: a record with one (request) => Response handler per enabled platform. Mount them all under a single dynamic segment:

app/api/channels/[platform]/route.ts
import { channels } from "@/lib/channels";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ platform: string }> },
) {
  const { platform } = await params;
  const handler = channels.webhooks[platform];
  if (!handler) {
    return new Response("unknown platform", { status: 404 });
  }
  return handler(request);
}

That is the whole integration surface. Adding a platform later means adding one entry to platforms in createChannels — the route never changes. When the handler returns, the inbound message is already a canonical channel_messages record on InstantDB, so any mounted ChannelTimeline shows it reactively the same instant.

Respond fast, process in the background

Platforms enforce tight webhook deadlines (Slack retries after 3 seconds) — but an agent reaction can take much longer than that. Each handler accepts a second options argument: pass a waitUntil function and the handler acknowledges the platform immediately while the inbound pipeline (persist → resolveContextIdreact → reply) continues after the response is flushed. In Next.js, wire it to after():

app/api/channels/[platform]/route.ts
import { after } from "next/server";
import { channels } from "@/lib/channels";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ platform: string }> },
) {
  const { platform } = await params;
  const handler = channels.webhooks[platform];
  if (!handler) {
    return new Response("unknown platform", { status: 404 });
  }
  return handler(request, {
    waitUntil: (promise: Promise<unknown>) => after(promise),
  });
}
Without waitUntil, the handler still works — it just keeps the request open until the reaction finishes. Fine in development; on serverless platforms with retrying webhooks, pass it so slow reactions never look like failed deliveries.

Signature verification

You never verify requests yourself. Each platform handler runs that platform's own verification internally — Slack's signing-secret HMAC, Telegram's secret token, and so on — using the credentials you passed in platforms. Requests that fail verification are rejected before any message is persisted or any reaction runs.

Point each platform at the route

With the route deployed, register the public URL in each platform's app configuration. The path segment must match the key you used in platforms:

platformwebhook urlwhere to configure
slackhttps://your-app.com/api/channels/slackapi.slack.com/apps → your app → Event Subscriptions → Request URL
teamshttps://your-app.com/api/channels/teamsAzure portal → your bot resource → Configuration → Messaging endpoint
gchathttps://your-app.com/api/channels/gchatGoogle Cloud Console → Chat API → Configuration → HTTP endpoint URL
discordhttps://your-app.com/api/channels/discordDiscord Developer Portal → your application → General Information → Interactions Endpoint URL
telegramhttps://your-app.com/api/channels/telegramBot API call: setWebhook with your URL (no dashboard — one HTTPS request)
terminal — telegram webhook registration
curl "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
  -d "url=https://your-app.com/api/channels/telegram"
Platforms verify the URL at registration time (Slack sends a challenge, Discord sends a signed ping). Deploy the route first, then register the URL — the handlers answer those verification handshakes automatically.

Troubleshooting

404 "unknown platform". The path segment did not match any key in channels.webhooks. Check the registered URL for typos (/api/channels/slack, not /api/channel/slack) and confirm the segment matches the key you used in the platforms object exactly.

Platform missing from the runtime. channels.webhooks only contains the platforms you configured in createChannels. If a platform is absent, it was not in platforms when the runtime booted — log channels.platforms on startup to see what was actually enabled, and check that the adapter package for that platform is installed (they are optional peers).

Webhook registers but messages never arrive. Verify the route is deployed and publicly reachable, then check credentials: a wrong signing secret means every request fails verification silently from the platform's point of view (it sees non-2xx responses and eventually stops retrying).

Next

database…