channel / guides / webhooks
Mount webhooks.
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:
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 → resolveContextId → react → reply) continues after the response is flushed. In Next.js, wire it to after():
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),
});
}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:
| platform | webhook url | where to configure |
|---|---|---|
| slack | https://your-app.com/api/channels/slack | api.slack.com/apps → your app → Event Subscriptions → Request URL |
| teams | https://your-app.com/api/channels/teams | Azure portal → your bot resource → Configuration → Messaging endpoint |
| gchat | https://your-app.com/api/channels/gchat | Google Cloud Console → Chat API → Configuration → HTTP endpoint URL |
| discord | https://your-app.com/api/channels/discord | Discord Developer Portal → your application → General Information → Interactions Endpoint URL |
| telegram | https://your-app.com/api/channels/telegram | Bot API call: setWebhook with your URL (no dashboard — one HTTPS request) |
curl "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
-d "url=https://your-app.com/api/channels/telegram"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).