# Channels

> HTTP and messaging ingress with defineChannel and platform factories (eve, slack, discord, teams, telegram, twilio, github), route verbs, webhook verification, and eve channels add/list scaffolding.

- Repository: vercel/eve
- GitHub: https://github.com/vercel/eve
- Human docs: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f/llms-full.txt

## Source Files

- `docs/channels/overview.mdx`
- `docs/channels/custom.mdx`
- `packages/eve/src/public/channels/index.ts`
- `packages/eve/src/compiler/normalize-channel.ts`
- `packages/eve/src/internal/nitro/routes/channel-dispatch.ts`
- `packages/eve/src/cli/commands/channels.ts`

---

---
title: "Channels"
description: "HTTP and messaging ingress with defineChannel and platform factories (eve, slack, discord, teams, telegram, twilio, github), route verbs, webhook verification, and eve channels add/list scaffolding."
---

Channels are the ingress layer between external platforms and the Eve runtime. Each channel module under `agent/channels/` compiles into one or more Nitro-mounted routes; route handlers call `send()` to start or resume sessions, and event handlers deliver runtime output back to the platform. The framework enables the default Eve HTTP channel even when `agent/channels/eve.ts` is absent, and merges authored routes with framework defaults at build time.

## Channel contract

A channel adapter performs three jobs:

1. **Normalize** platform input into a user message (string or `UserContent`).
2. **Own the continuation token** — the channel-local resume handle for a conversation on that surface. The runtime namespaces tokens as `<channelName>:<rawToken>` (for example `slack:C0123ABC:1800000000.001234`).
3. **Deliver** responses — post messages, stream events, or acknowledge webhooks depending on the platform.

Local subagents do not declare channels. Only the root agent under `agent/channels/` registers ingress routes.

```mermaid
flowchart LR
  subgraph ingress["Ingress"]
    Platform["Platform webhook / client"]
    Route["Nitro channel route"]
  end
  subgraph runtime["Eve runtime"]
    Send["send()"]
    Session["Durable session"]
    Events["Channel event handlers"]
  end
  Platform --> Route
  Route --> Send
  Send --> Session
  Session --> Events
  Events --> Platform
```

## File location and identity

Channel files live at the root agent only:

:::files
agent/
  agent.ts
  channels/
    eve.ts          # optional override of default HTTP channel
    slack.ts
    intake.ts
:::

The file stem is the channel id — `agent/channels/intake.ts` is addressed as `intake`. Export the channel as the module's default export. Do not add a `name` field; Eve derives identifiers from the filesystem path.

Each route in a `defineChannel` `routes` array becomes a separate compiled manifest entry with its own `(method, urlPath)` pair. The compiler records the channel name from the path and the URL path from the route definition.

## Authoring with defineChannel

Import `defineChannel` and route helpers from `eve/channels`:

```ts
import { defineChannel, GET, POST } from "eve/channels";

export default defineChannel({
  routes: [
    POST("/message", async (req, { send }) => {
      const body = await req.json();
      const session = await send(body.message, {
        auth: null,
        continuationToken: body.token,
      });
      return Response.json({ sessionId: session.id });
    }),
    GET("/sessions/:sessionId/stream", async (_req, { getSession, params }) => {
      const session = getSession(params.sessionId);
      const stream = await session.getEventStream();
      return new Response(stream, {
        headers: { "content-type": "application/x-ndjson; charset=utf-8" },
      });
    }),
  ],
  events: {
    "message.completed"(event, channel, ctx) {
      // deliver completed messages back to the surface
    },
  },
});
```

### Route verbs

Declare HTTP routes with `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. Each handler receives the raw `Request` and a helpers object:

| Helper | Purpose |
| --- | --- |
| `send(message, options)` | Start or resume a session on this channel |
| `getSession(sessionId)` | Look up an existing session (for streaming) |
| `receive(targetChannel, input)` | Hand inbound work to another channel |
| `params` | Path parameters from `[name]` segments |
| `waitUntil(promise)` | Extend request lifetime for post-ack background work |
| `requestIp` | Client IP, or `null` when unavailable |

WebSocket routes use `WS(path, handler)`. The handler runs once per upgrade and returns lifecycle hooks (`upgrade`, `open`, `message`, `close`, `error`). For third-party SDKs that bind directly to a Node `http.Server`, `createWebSocketUpgradeServer()` provides a compatibility bridge.

### ChannelDefinition fields

<ParamField body="routes" type="RouteDefinition[]" required>
HTTP and WebSocket route descriptors. Required on every channel.
</ParamField>

<ParamField body="state" type="object">
Seeds durable adapter state for stateful channels. Passed to `send()` via `options.state` on first dispatch.
</ParamField>

<ParamField body="context(state, session)" type="function">
Builds the per-step `channel` argument for event handlers. Can close over `session` to call `setContinuationToken()` later.
</ParamField>

<ParamField body="events" type="ChannelEvents">
Lifecycle handlers keyed by stream event type (`message.completed`, `turn.started`, `input.requested`, etc.). `session.failed` receives only `(data, channel)` — no `ctx`.
</ParamField>

<ParamField body="receive(input, { send })" type="function">
Accepts cross-channel handoffs from `receive()` in another channel's route handler.
</ParamField>

<ParamField body="fetchFile(url)" type="function">
Fetches bytes for `FilePart.data` URL objects before sandbox staging. Return `null` to pass the URL through to the model provider.
</ParamField>

<ParamField body="metadata(state)" type="function">
Projects a JSON-safe subset of adapter state for instrumentation and dynamic resolvers (`ctx.channel.metadata`, narrowed with `isChannel`).
</ParamField>

### Continuation tokens

Pass the channel-local raw token to `send()`. The framework prepends the channel name before handing it to the runtime. Platform factories ship helpers such as `slackContinuationToken(channelId, threadTs)` and `twilioContinuationToken(from, to)`. Custom channels define their own join format.

Re-key a parked session with `channel.setContinuationToken(rawToken)` or `session.setContinuationToken(rawToken)` from a `context()` factory. Inbound deliveries addressed to the old token are dropped after the next step boundary.

## Default Eve HTTP channel

The Eve channel is the framework's default HTTP session API — the routes the terminal UI, `eve/client`, `useEveAgent`, and `curl` use. It is enabled by default without an authored file. Add `agent/channels/eve.ts` only to override behavior (most often route auth).

`eveChannel()` from `eve/channels/eve` wraps `defineChannel` and mounts:

| Method | Path | Behavior |
| --- | --- | --- |
| `POST` | `/eve/v1/session` | Create session; returns `202` with `sessionId` and `continuationToken` |
| `POST` | `/eve/v1/session/:sessionId` | Continue session with message and/or `inputResponses` |
| `GET` | `/eve/v1/session/:sessionId/stream` | NDJSON event stream (`startIndex` query param supported) |

When no authored `eve.ts` exists, the framework injects `eveChannel({ auth: [localDev(), vercelOidc()] })` — the same defaults the web scaffold writes.

```ts
import { eveChannel } from "eve/channels/eve";
import { localDev, placeholderAuth, vercelOidc } from "eve/channels/auth";

export default eveChannel({
  auth: [localDev(), vercelOidc(), placeholderAuth()],
});
```

`auth` is required. Pass a single `AuthFn` or an ordered array walked by `routeAuth`: the first entry returning a `SessionAuthContext` wins; `null`/`undefined` skips to the next; exhaustion returns `401`. Include `none()` last for anonymous traffic.

## Platform channel factories

Built-in channels are `defineChannel` instances with platform-specific verification, parsing, and delivery. Default-export the factory result from `agent/channels/<name>.ts`.

| Factory | Import path | Default route | Verification |
| --- | --- | --- | --- |
| `eveChannel` | `eve/channels/eve` | `/eve/v1/session`, `/eve/v1/session/:sessionId`, `/eve/v1/session/:sessionId/stream` | `routeAuth` (JWT, OIDC, Basic, etc.) |
| `slackChannel` | `eve/channels/slack` | `POST /eve/v1/slack` | Slack v0 HMAC (`X-Slack-Signature`) or custom `webhookVerifier` |
| `discordChannel` | `eve/channels/discord` | `POST /eve/v1/discord` | Ed25519 (`X-Signature-Ed25519`) or `webhookVerifier` |
| `teamsChannel` | `eve/channels/teams` | `POST /eve/v1/teams` | Bot Framework JWT or `webhookVerifier` |
| `telegramChannel` | `eve/channels/telegram` | `POST /eve/v1/telegram` | `X-Telegram-Bot-Api-Secret-Token` or `webhookVerifier` |
| `twilioChannel` | `eve/channels/twilio` | `POST /eve/v1/twilio/messages`, `/voice`, `/voice/transcription` | `X-Twilio-Signature` over URL + sorted form params |
| `githubChannel` | `eve/channels/github` | `POST /eve/v1/github` | `X-Hub-Signature-256` HMAC or `webhookVerifier` |

<Note>
`linearChannel` (`eve/channels/linear`, `POST /eve/v1/linear`) is also shipped but is not in the platform factory list above. Author it the same way when you need Linear Agent Sessions.
</Note>

Each factory accepts an optional `route` override, `credentials` for secrets, inbound hooks (`onAppMention`, `onCommand`, `onComment`, etc.), `events` overrides, and `uploadPolicy` for inbound attachments. Platform channels that receive webhooks typically acknowledge immediately and drive `send()` inside `waitUntil()` so the platform does not time out.

### Webhook verification pattern

Platform verifiers share a contract:

- **Throw or return falsy** → channel responds `401`.
- **Return a string** → accept and use that string as the verified body.
- **Return other truthy** → accept and keep the original body.

All built-in verifiers also support an optional `webhookVerifier` callback for Connect-forwarded traffic authenticated with Vercel OIDC instead of the platform signing secret.

<Warning>
Twilio signs the exact public URL plus sorted POST parameters. Set `webhookUrl` when a proxy or tunnel rewrites `request.url`, or signature checks fail even with a valid auth token.
</Warning>

### Example: Slack channel

```ts
import { slackChannel } from "eve/channels/slack";

export default slackChannel({
  credentials: {
    botToken: process.env.SLACK_BOT_TOKEN,
    signingSecret: process.env.SLACK_SIGNING_SECRET,
  },
});
```

The default route handles Slack Events API callbacks and `application/x-www-form-urlencoded` interaction payloads on the same path.

## Runtime dispatch

`dispatchChannelRequest` in the Nitro host looks up the channel by `(method, urlPath)` route key, builds `RouteHandlerArgs` (`send`, `getSession`, `receive`, `params`, `waitUntil`, `requestIp`), and invokes the compiled handler. Background tasks registered through `waitUntil` are forwarded to `event.waitUntil()` so webhook acknowledgements can return before session work completes.

Authored channels carry a `handler` field. Framework-internal routes (connection OAuth callbacks, session callbacks) use a `fetch`-only shape with `RouteContext.agent`.

At build time, `computeChannelRouteRegistrations` merges framework defaults first, then authored routes. An authored file with the same channel name as a framework default replaces that default entirely.

## Disabling framework routes

Export `disableRoute()` as the default export of `agent/channels/<name>.ts` to remove a framework default route whose logical name matches the file stem:

```ts
import { disableRoute } from "eve/channels";

export default disableRoute();
```

Valid targets are framework channel names (`eve`, connection callback routes, session callback routes). Disabling a non-framework name throws at build time.

## Cross-channel hand-off

Route handlers can pivot work onto another channel:

```ts
import { defineChannel, POST } from "eve/channels";
import slack from "./slack.js";

export default defineChannel({
  routes: [
    POST("/incident", async (req, args) => {
      const incident = await req.json();
      args.waitUntil(
        args.receive(slack, {
          message: `Investigate ${incident.reference}`,
          target: { channelId: "C0123ABC" },
          auth: { authenticator: "incidentio", principalType: "service", principalId: incident.actor.id, attributes: {} },
        }),
      );
      return new Response("ok");
    }),
  ],
});
```

The target channel's `receive` hook owns continuation-token format and initial state. Calling `receive` does not also start a session on the current channel.

## CLI scaffolding

### eve channels add

`eve channels add` composes channel selection, file scaffolding, Vercel services configuration, and optional deploy. Known kinds for non-interactive use:

<ParamField body="kind" type="slack | web" required>
Channel kind. Required when stdin is not a TTY or when `--yes` is passed.
</ParamField>

<ParamField body="--force" type="boolean">
Overwrite existing scaffold files.
</ParamField>

<ParamField body="--yes" type="boolean">
Assume yes for confirmations (e.g. Slackbot creation). Requires an explicit kind.
</ParamField>

```sh
eve channels add              # interactive picker (web, Slack)
eve channels add slack        # scaffold Slack channel + connector setup
eve channels add web          # scaffold Next.js web chat + agent/channels/eve.ts
```

The `web` kind scaffolds `agent/channels/eve.ts` with `[localDev(), vercelOidc(), placeholderAuth()]`, a Next.js chat UI, and Vercel services config. The `slack` kind scaffolds `agent/channels/slack.ts` and provisions a Slack connector against the linked Vercel project.

### eve channels list

Lists channel module stems discovered under `agent/channels/` (flat `.ts`/`.mts` files and folder-based modules).

<ParamField body="--json" type="boolean">
Emit `{ "channels": ["eve", "slack", ...] }`.
</ParamField>

```sh
eve channels list
eve channels list --json
```

When no channels exist, the command suggests running `eve channels add`.

## Choosing a channel

| Goal | Channel |
| --- | --- |
| Browser chat, SDK clients, `curl` | Eve channel (default) + client hooks |
| Slack mentions, DMs, buttons | `slackChannel` |
| Discord slash commands, components | `discordChannel` |
| Microsoft Teams messages, Adaptive Cards | `teamsChannel` |
| Telegram bot messages | `telegramChannel` |
| SMS or speech-transcribed calls | `twilioChannel` |
| GitHub @mentions, PR review | `githubChannel` |
| Internal webhook, WebSocket, custom transport | `defineChannel` |

<Info>
Eve uses Chat SDK card-builder primitives for rich Slack messages but does not use the Chat SDK runtime. Wire channels against Eve's `defineChannel` API, not a Chat SDK adapter.
</Info>

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| `401` on platform webhook | Missing or wrong signing secret; clock skew beyond 5 minutes; custom `webhookVerifier` returned falsy |
| `401` on `/eve/v1/session` | `routeAuth` exhausted without a winner; replace `placeholderAuth()` in production |
| Route not found (`404`) | Route path or method mismatch; run `eve info` or `eve build` and check compiled channel entries |
| `disableRoute()` build error | File stem does not match a framework channel name |
| Twilio signature mismatch | `webhookUrl` does not match the URL Twilio configured |
| Session not resuming | Continuation token format changed; old-token deliveries dropped after re-key |

## Related pages

<CardGroup>
  <Card title="Sessions and streaming" href="/sessions-and-streaming">
    continuationToken contracts, NDJSON stream events, and follow-up routes on the Eve channel.
  </Card>
  <Card title="Security model" href="/security-model">
    Route auth, webhook verification, secret brokering, and fail-closed defaults.
  </Card>
  <Card title="Auth and deployment" href="/auth-and-deployment">
    `eveChannel` auth walk, env vars, `eve link`/`eve deploy`, and production verification.
  </Card>
  <Card title="HTTP API" href="/http-api">
    Stable `/eve/v1` request and response shapes for session create, continue, and stream.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full `eve channels` flags, exit codes, and the edit-info-dev-build-start loop.
  </Card>
  <Card title="TypeScript API" href="/typescript-api">
    Exported `define*` helpers, `AuthFn` strategies, and channel factory types.
  </Card>
</CardGroup>
